Skip to content

Commit

Permalink
Add the ability to search through belongs_to, has_one and has_many fi…
Browse files Browse the repository at this point in the history
…elds
  • Loading branch information
jonatasrancan committed Jan 16, 2018
1 parent f2dce70 commit 76d5087
Show file tree
Hide file tree
Showing 9 changed files with 160 additions and 9 deletions.
32 changes: 32 additions & 0 deletions docs/customizing_dashboards.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,22 @@ than one column. e.g.: `"name, email DESC"`.
`:foreign_key` - Specifies the name of the foreign key directly.
Defaults to `:#{attribute}_id`.

`:searchable` - Specify if the attribute should be considered when searching.
Default is `false`.

`searchable_field` - Specify which column to use on the search, only applies if `searchable` is `true`

For example:

```ruby
country: Field::BelongsTo(
searchable: true,
seachable_field: 'name',
)
```

with this, you will be able to search through the column `name` from the association `belongs_to :country`, from your model.

**Field::HasMany**

`:limit` - Set the number of resources to display in the show view. Default is
Expand All @@ -93,6 +109,22 @@ Defaults to `:#{attribute}_id`.

`:foreign_key` - Specifies the name of the foreign key directly. Defaults to `:#{attribute}_id`

`:searchable` - Specify if the attribute should be considered when searching.
Default is `false`.

`searchable_field` - Specify which column to use on the search, only applies if `searchable` is `true`

For example:

```ruby
cities: Field::HasMany(
searchable: true,
seachable_field: 'name',
)
```

with this, you will be able to search through the column `name` from the association `has_many :cities`, from your model.

**Field::Number**

`:decimals` - Set the number of decimals to display. Defaults to `0`.
Expand Down
4 changes: 4 additions & 0 deletions lib/administrate/field/deferred.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ def searchable?
options.fetch(:searchable, deferred_class.searchable?)
end

def searchable_field
options.fetch(:searchable_field)
end

def permitted_attribute(attr, _options = nil)
options.fetch(:foreign_key,
deferred_class.permitted_attribute(attr, options))
Expand Down
4 changes: 3 additions & 1 deletion lib/administrate/order.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ def apply(relation)
return order_by_association(relation) unless
reflect_association(relation).nil?

return relation.reorder("#{attribute} #{direction}") if
order = "#{relation.table_name}.#{attribute} #{direction}"

return relation.reorder(order) if
relation.columns_hash.keys.include?(attribute.to_s)

relation
Expand Down
42 changes: 38 additions & 4 deletions lib/administrate/search.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,17 @@ def run
if @term.blank?
@scoped_resource.all
else
@scoped_resource.where(query, *search_terms)
@scoped_resource.joins(tables_to_join).where(query, *search_terms)
end
end

private

def query
search_attributes.map do |attr|
table_name = ActiveRecord::Base.connection.
quote_table_name(@scoped_resource.table_name)
attr_name = ActiveRecord::Base.connection.quote_column_name(attr)
table_name = query_table_name(attr)
attr_name = column_to_query(attr)

"lower(#{table_name}.#{attr_name}) LIKE ?"
end.join(" OR ")
end
Expand All @@ -42,6 +42,40 @@ def attribute_types
@dashboard_class::ATTRIBUTE_TYPES
end

def query_table_name(attr)
if association_search?(attr)
ActiveRecord::Base.connection.quote_table_name(attr.to_s.pluralize)
else
ActiveRecord::Base.connection.
quote_table_name(@scoped_resource.table_name)
end
end

def column_to_query(attr)
if association_search?(attr)
ActiveRecord::Base.connection.
quote_column_name(attribute_types[attr].searchable_field)
else
ActiveRecord::Base.connection.quote_column_name(attr)
end
end

def tables_to_join
attribute_types.keys.select do |attribute|
attribute_types[attribute].searchable? && association_search?(attribute)
end
end

def association_search?(attribute)
return unless attribute_types[attribute].respond_to?(:deferred_class)

[
Administrate::Field::BelongsTo,
Administrate::Field::HasMany,
Administrate::Field::HasOne,
].include?(attribute_types[attribute].deferred_class)
end

attr_reader :resolver, :term
end
end
7 changes: 6 additions & 1 deletion spec/example_app/app/dashboards/customer_dashboard.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ class CustomerDashboard < Administrate::BaseDashboard
updated_at: Field::DateTime,
kind: Field::Select.with_options(collection: Customer::KINDS),
country: Field::BelongsTo.
with_options(primary_key: :code, foreign_key: :country_code),
with_options(
primary_key: :code,
foreign_key: :country_code,
searchable: true,
searchable_field: "name",
),
}

COLLECTION_ATTRIBUTES = ATTRIBUTE_TYPES.keys
Expand Down
6 changes: 6 additions & 0 deletions spec/factories.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
FactoryBot.define do
factory :customer do
country
sequence(:name) { |n| "Customer #{n}" }
email { name.downcase.gsub(" ", "_") + "@example.com" }

Expand Down Expand Up @@ -61,4 +62,9 @@
factory :series do
sequence(:name) { |n| "Series #{n}" }
end

factory :country do
sequence(:name) { |n| "Country #{n}" }
sequence(:code) { |n| "C#{n}" }
end
end
15 changes: 15 additions & 0 deletions spec/features/search_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,21 @@
expect(page).to have_content(mismatch.email)
end

scenario "admin searches across associations fields", :js do
country = create(:country, name: "Brazil", code: "BR")
country_match = create(:customer, country: country)
mismatch = create(:customer)

visit admin_customers_path

fill_in :search, with: "Brazil"
submit_search

expect(page).to have_content(country_match.email)
expect(page).to have_content(country.name)
expect(page).not_to have_content(mismatch.email)
end

def clear_search
find(".search__clear-link").click
end
Expand Down
5 changes: 3 additions & 2 deletions spec/lib/administrate/order_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@

ordered = order.apply(relation)

expect(relation).to have_received(:reorder).with("name asc")
expect(relation).to have_received(:reorder).with("table_name.name asc")
expect(ordered).to eq(relation)
end

Expand All @@ -47,7 +47,7 @@

ordered = order.apply(relation)

expect(relation).to have_received(:reorder).with("name desc")
expect(relation).to have_received(:reorder).with("table_name.name desc")
expect(ordered).to eq(relation)
end
end
Expand Down Expand Up @@ -171,6 +171,7 @@ def relation_with_column(column)
double(
klass: double(reflect_on_association: nil),
columns_hash: { column.to_s => :column_info },
table_name: "table_name",
)
end

Expand Down
54 changes: 53 additions & 1 deletion spec/lib/administrate/search_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,20 @@ class MockDashboard
name: Administrate::Field::String,
email: Administrate::Field::Email,
phone: Administrate::Field::Number,
}
}.freeze
end

class MockDashboardWithAssociation
ATTRIBUTE_TYPES = {
role: Administrate::Field::BelongsTo.with_options(
searchable: true,
searchable_field: "name",
),
address: Administrate::Field::HasOne.with_options(
searchable: true,
searchable_field: "street",
),
}.freeze
end

describe Administrate::Search do
Expand Down Expand Up @@ -86,5 +99,44 @@ class User < ActiveRecord::Base; end
remove_constants :User
end
end

context "when searching through associations" do
let(:scoped_object) { double(:scoped_object) }

let(:search) do
Administrate::Search.new(
scoped_object,
MockDashboardWithAssociation,
"Тест Test",
)
end

let(:expected_query) do
[
"lower(\"roles\".\"name\") LIKE ?"\
" OR lower(\"addresses\".\"street\") LIKE ?",
"%тест test%",
"%тест test%",
]
end

it "joins with the correct association table to query" do
allow(scoped_object).to receive(:where)

expect(scoped_object).to receive(:joins).with(%i(role address)).
and_return(scoped_object)

search.run
end

it "builds the 'where' clause using the joined tables" do
allow(scoped_object).to receive(:joins).with(%i(role address)).
and_return(scoped_object)

expect(scoped_object).to receive(:where).with(*expected_query)

search.run
end
end
end
end

0 comments on commit 76d5087

Please sign in to comment.