Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,14 @@ end
+ `attribute_types`: hash used to specify the filter fields, ex. `{ title: Administrate::Field::String }`
+ `search_path`: the path to use for searching (form URL)
+ `namespace`: the namespace used by Administrate, ex. `:supervisor`
- For associations (_has many_/_belongs to_) the label used can be customized adding an `admin_label` method to the target model which returns a string while the collection can by filtered with `admin_scope`. Example:
- For associations (_has many_/_belongs to_) the label used can be customized adding an `display_resource` method to the target dashboard which returns a string. Example:

```rb
# Sample post model
class Post < ApplicationRecord
scope :admin_scope, -> { where(published: true) }

def admin_label
title.upcase
class PostDashboard < Administrate::BaseDashboard
# Overwrite this method to customize how posts are displayed
# across all pages of the admin dashboard.
def display_resource(post)
"##{post.id} #{post.title&.upcase}"
end
end
```
Expand Down Expand Up @@ -139,9 +138,11 @@ end
# In alternative prepare an hash in the dashboard like RANSACK_TYPES = {}
attribute_types = {
title: Administrate::Field::String,
title_or_description_cont: Administrate::Field::String,
author: Administrate::Field::BelongsTo,
category: Administrate::Field::Select.with_options(collection: Post.categories.to_a),
published: Administrate::Field::Boolean
published: Administrate::Field::Boolean,
updated_at_lteq: Administrate::Field::Date
}
attribute_labels = {
author: 'Written by',
Expand Down
2 changes: 1 addition & 1 deletion app/views/administrate_ransack/_filters.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<% attribute_types.each do |field, type| %>
<% next if field == :id %>

<% label = attribute_labels.include?(field) ? attribute_labels[field] : field %>
<% label = attribute_labels.include?(field) ? attribute_labels[field] : nil %>
<% model = @ransack_results.klass %>
<% input_type = type.is_a?(Administrate::Field::Deferred) ? type.deferred_class.to_s : type.to_s %>
<% component = AdministrateRansack::FILTERS[input_type] || 'field_other' %>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
<% association = model.reflections[field.to_s] %>
<% if association %>
<% field_key = model.ransackable_scopes.include?(field) ? field : "#{field}_id_eq" %>
<% desc = association.klass.method_defined?(:admin_label) ? :admin_label : :to_s %>
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a breaking change, I prefer to have a specific PR for it. And it can be merged only with the next major version.

<% collection = association.klass.send(association.klass.respond_to?(:admin_scope) ? :admin_scope : :all) %>
<% field_key = AdministrateRansack.ransack?(model, {field => "1,2"}) ? field : "#{field}_id_eq" %>
<% label ||= AdministrateRansack.ransack?(model, {field => "1,2"}) ? field : "#{field}_id" %>
<% resource_field = type.new(field, nil, Administrate::Page::Collection.new(@dashboard), resource: model.new) %>
<% collection = resource_field.associated_resource_options %>

<%= form.label(label, class: 'filter-label') %>
<%= form.collection_select(field_key, collection, :id, desc, include_blank: true) %>
<%= form.select("#{field}_id_eq", collection, { include_blank: true }, { class: 'selectize' }) %>
<% end %>
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<% field_key = model.ransackable_scopes.include?(field) ? field : "#{field}_eq" %>
<% field_key = AdministrateRansack.ransack?(model, {field => "true"}) ? field : "#{field}_eq" %>
<% values = [[t('administrate_ransack.filters.no'), false], [t('administrate_ransack.filters.yes'), true]] %>

<%= form.label(label, class: 'filter-label') %>
<%= form.label(label || field, class: 'filter-label') %>
<%= form.select(field_key, values, include_blank: true) %>
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<%= form.label(label, class: 'filter-label') %>
<% if model.ransackable_scopes.include?(field) %>
<%= form.label(label || field, class: 'filter-label') %>
<% if AdministrateRansack.ransack?(model, {field => Date.today}) %>
<%= form.date_field(field, value: form.object.send(field)) %>
<% else %>
<%= form.date_field("#{field}_gteq") %>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<%= form.label(label, class: 'filter-label') %>
<% if model.ransackable_scopes.include?(field) %>
<%= form.label(label || field, class: 'filter-label') %>
<% if AdministrateRansack.ransack?(model, {field => DateTime.now}) %>
<%= form.datetime_field(field, value: form.object.send(field)) %>
<% else %>
<%= form.datetime_field("#{field}_gteq") %>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
<% association = model.reflections[field.to_s] %>
<% if association %>
<% field_key = model.ransackable_scopes.include?(field) ? field : "#{field}_id_in" %>
<% desc = association.klass.method_defined?(:admin_label) ? :admin_label : :to_s %>
<% collection = association.klass.send(association.klass.respond_to?(:admin_scope) ? :admin_scope : :all) %>
<% field_key = AdministrateRansack.ransack?(model, {field => "1,2"}) ? field : "#{field}_id_in" %>
<% resource_field = type.new(field, nil, Administrate::Page::Collection.new(@dashboard), resource: model.new) %>
<% collection = resource_field.associated_resource_options %>

<%= form.label(label, class: 'filter-label') %>
<%= form.label(label || field_key, class: 'filter-label') %>
<% if options&.include? 'select' %>
<%= form.select(field_key, nil, {}, multiple: true) do %>
<%= options_from_collection_for_select(collection, :id, desc) %>
<% end %>
<%= form.select(field_key, collection, {}, multiple: true) %>
<% else %>
<%= form.collection_check_boxes(field_key, collection, :id, desc) do |b| %>
<%= form.collection_check_boxes(field_key, collection, :second, :first) do |b| %>
<%= b.label do %>
<%= b.check_box %>
<span><%= b.object.send(desc) %></span>
<span><%= b.text %></span>
<% end %>
<% end %>
<% end %>
Expand Down
11 changes: 7 additions & 4 deletions app/views/administrate_ransack/components/_field_number.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
<% field_key = model.ransackable_scopes.include?(field) ? field : "#{field}_eq" %>

<%= form.label(label, class: 'filter-label') %>
<%= form.number_field(field_key) %>
<%= form.label(label || field, class: 'filter-label') %>
<% if AdministrateRansack.ransack?(model, {field => "1"}) %>
<%= form.number_field "#{field}" %>
<% else %>
<%= form.number_field "#{field}_gteq" %>
<%= form.number_field "#{field}_lteq" %>
<% end %>
11 changes: 9 additions & 2 deletions app/views/administrate_ransack/components/_field_other.html.erb
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
<%= form.label(label, class: 'filter-label') %>
<%= form.search_field(field) %>
<% if AdministrateRansack.ransack?(model, {field => "valid"}) %>
<%= form.label(label || field, class: 'filter-label') %>
<%= form.search_field(field) %>
<% elsif AdministrateRansack.ransack?(model, {"#{field}_cont" => "valid"}) %>
<%= form.label(label || "#{field}_cont", class: 'filter-label') %>
<%= form.search_field("#{field}_cont") %>
<% else %>
<%# render nothing %>
<% end %>

<%# unsupported Field::HasOne %>
<%# unsupported Field::Polymorphic %>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<% field_key = model.ransackable_scopes.include?(field) ? field : "#{field}_eq" %>
<% collection = (type.respond_to?(:options) ? type.options[:collection] : []) || [] %>

<%= form.label(label, class: 'filter-label') %>
<%= form.label(label || field, class: 'filter-label') %>
<%= form.select(field_key, collection, include_blank: true) %>
13 changes: 9 additions & 4 deletions app/views/administrate_ransack/components/_field_string.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
<% field_key = model.ransackable_scopes.include?(field) ? field : "#{field}_cont" %>

<%= form.label(label, class: 'filter-label') %>
<%= form.search_field(field_key) %>
<% if AdministrateRansack.ransack?(model, {field => "valid"}) %>
<%= form.label(label || field, class: 'filter-label') %>
<%= form.search_field(field) %>
<% elsif AdministrateRansack.ransack?(model, {"#{field}_cont" => "valid"}) %>
<%= form.label(label || "#{field}_cont", class: 'filter-label') %>
<%= form.search_field("#{field}_cont") %>
<% else %>
<%# render nothing %>
<% end %>
1 change: 1 addition & 0 deletions lib/administrate_ransack.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require 'administrate_ransack/engine'
require 'administrate_ransack/filters'
require 'administrate_ransack/helpers'
require 'administrate_ransack/searchable'
require 'administrate_ransack/version'

Expand Down
10 changes: 10 additions & 0 deletions lib/administrate_ransack/helpers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# frozen_string_literal: true

module AdministrateRansack
class << self
def ransack?(model, params = {}, options = {})
ransack = model.ransack(params, **options)
ransack.instance_variable_get(:@scope_args).present? || ransack.base.c.present?
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The general idea is good but I prefer to avoid accessing internal Ransack details (like @scope_args instance var) because they do not belong to the public API of the gem.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this is a feature currently missing in Ransack, and I'm hoping it will eventually be implemented there — though it hasn't happened yet (there are some related PRs, though).

It might be faster to contribute directly to Ransack, but if we want to avoid that for now, how about something like the following as a workaround?

def ransack?(model, params, **options)
  begin
    model.ransack!(params, **options)
    true
  rescue StandardError
    false
  end
end

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mmm... perhaps you could try something like:

  def ransack?(model, field)
    ctx = Ransack::Context.for(model)
    ctx.ransackable_attribute?(field, model) ||
      ctx.ransackable_association?(field, model) ||
      ctx.ransackable_scope?(field, model)
  end

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, that only checks the model definition, but it doesn’t reflect which field names can actually be used in Ransack.

One of the goals of this PR is to support Ransack-style search conditions like name_or_description_or_email_or_articles_title_cont.
To do that, we need to determine whether a given field is usable after it has gone through all of Ransack’s initialization processes, including things like groupings.

end
end
end
2 changes: 2 additions & 0 deletions spec/dummy/app/controllers/admin/authors_controller.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
module Admin
class AuthorsController < Admin::ApplicationController
prepend AdministrateRansack::Searchable

# Overwrite any of the RESTful controller actions to implement custom behavior
# For example, you may want to send an email after a foo is updated.
#
Expand Down
10 changes: 10 additions & 0 deletions spec/dummy/app/dashboards/author_dashboard.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ class AuthorDashboard < Administrate::BaseDashboard
# published_posts
# recent_posts

# RANSACK_TYPES
RANSACK_TYPES = {
posts: Field::HasMany,
tags: Field::HasMany,
name: Field::String,
name_or_email_cont: Field::String,
name_not_cont: Field::String,
age: Field::Number
}.freeze

# SHOW_PAGE_ATTRIBUTES
# an array of attributes that will be displayed on the model's show page.
SHOW_PAGE_ATTRIBUTES = %i[
Expand Down
6 changes: 3 additions & 3 deletions spec/dummy/app/dashboards/tag_dashboard.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ class TagDashboard < Administrate::BaseDashboard
# Overwrite this method to customize how tags are displayed
# across all pages of the admin dashboard.
#
# def display_resource(tag)
# "Tag ##{tag.id}"
# end
def display_resource(tag)
"##{tag.id} #{tag.name}"
end
end
17 changes: 17 additions & 0 deletions spec/dummy/app/views/admin/authors/index.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<section class="main-content__body main-content__body--flush">
<%= render(
"collection",
collection_presenter: page,
collection_field_name: resource_name,
page: page,
resources: resources,
table_title: "page-title"
) %>

<%= paginate resources, param_name: '_page' %>
</section>

<%= render(
'administrate_ransack/filters',
attribute_types: @dashboard.class::RANSACK_TYPES
) %>
1 change: 1 addition & 0 deletions spec/dummy/app/views/admin/posts/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
category: Administrate::Field::Select.with_options(collection: Post.categories.to_a),
published: Administrate::Field::Boolean,
position: Administrate::Field::Number,
position_eq: Administrate::Field::Number,
tags: Administrate::Field::HasMany,
dt: Administrate::Field::Date,
created_at: Administrate::Field::DateTime,
Expand Down
2 changes: 1 addition & 1 deletion spec/rails_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def setup_data
author = Author.find_by!(name: 'A test author')
tag = Tag.find_by!(name: 'A test tag')
Post.first.update!(title: 'A post', author: author, category: 'news', published: true, dt: Time.zone.today)
Post.second.update!(title: 'Another post', author: author, category: 'story', dt: Date.yesterday, tags: [tag])
Post.second.update!(title: 'Another post', author: author, category: 'story', position: 123, dt: Date.yesterday, tags: [tag])
Post.third.update!(title: 'Last post', author: author, category: 'news', position: 234, dt: Date.tomorrow)
end
end
Expand Down
12 changes: 12 additions & 0 deletions spec/system/number_filter_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@

RSpec.describe 'Number filter' do
let(:post3) { Post.third }
let(:author1) { Author.first }

it 'filters the authors by age range', :aggregate_failures do
visit '/admin/authors'

fill_in('q[age_lteq]', with: '28')
find('input[type="submit"]').click

expect(page).to have_current_path %r{/admin/authors\?.+q%5Bage_lteq%5D=28.*}
expect(page).to have_css('.js-table-row', count: 2)
expect(page).to have_css('.js-table-row a.action-show', text: author1.name)
end

it 'filters the posts by position', :aggregate_failures do
visit '/admin/posts'
Expand Down
27 changes: 27 additions & 0 deletions spec/system/string_filter_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,33 @@

RSpec.describe 'String filter' do
let(:post2) { Post.second }
let(:author11) { Author.find(11) }

it 'filters the authors by name or email', :aggregate_failures do
visit '/admin/authors'

fill_in('q[name_or_email_cont]', with: '@bbb')
find('input[type="submit"]').click

expect(page).to have_css('.js-table-row', count: 1)
expect(page).to have_css('.js-table-row a.action-show', text: author11.name)

fill_in('q[name_or_email_cont]', with: 'A test')
find('input[type="submit"]').click

expect(page).to have_css('.js-table-row', count: 1)
expect(page).to have_css('.js-table-row a.action-show', text: author11.name)
end

it 'filters the authors by name not contain', :aggregate_failures do
visit '/admin/authors'

fill_in('q[name_not_cont]', with: 'A test')
find('input[type="submit"]').click

expect(page).to have_css('.js-table-row', count: 10)
expect(page).not_to have_css('.js-table-row a.action-show', text: author11.name)
end

it 'filters the posts by title', :aggregate_failures do
visit '/admin/posts'
Expand Down