Skip to content

Commit

Permalink
Merge pull request #1928 from Daxter/1093-dropdown-for-habtm
Browse files Browse the repository at this point in the history
[1093] HABTM Select Filters
  • Loading branch information
seanlinsley committed May 24, 2013
2 parents b7cb887 + bbf6233 commit fe5becd
Show file tree
Hide file tree
Showing 9 changed files with 127 additions and 97 deletions.
10 changes: 0 additions & 10 deletions lib/active_admin/comments.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,6 @@
config.comments = false # Don't allow comments on comments
config.batch_actions = false # The default destroy batch action isn't showing up anyway...

if Rails::VERSION::STRING >= '3.2'
filter :resource_type, :as => :select, :collection => proc{ ActiveAdmin::Comment.uniq.pluck :resource_type }
filter :author_type, :as => :select, :collection => proc{ ActiveAdmin::Comment.uniq.pluck :author_type }
else
filter :resource_type
filter :author_type
end
filter :body
filter :created_at

scope :all, :show_count => false
# Register a scope for every namespace that exists.
# The current namespace will be the default scope.
Expand Down
3 changes: 2 additions & 1 deletion lib/active_admin/filters.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require 'active_admin/filters/dsl'
require "active_admin/filters/resource_extension"
require 'active_admin/filters/resource_extension'
require 'active_admin/filters/formtastic_addons'
require 'active_admin/filters/forms'

# Add our Extensions
Expand Down
72 changes: 31 additions & 41 deletions lib/active_admin/filters/forms.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,37 @@ module Filters
# This form builder defines methods to build filter forms such
# as the one found in the sidebar of the index page of a standard resource.
class FormBuilder < ::ActiveAdmin::FormBuilder
include ::ActiveAdmin::Filters::FormtasticAddons

def initialize(*args)
@use_form_buffer = true # force ActiveAdmin::FormBuilder to use the form buffer
super
end

def filter(method, options = {})
return "" if method.blank? ||
(options[:as] ||= default_input_type(method)).nil?
content = input(method, options)
form_buffers.last << content.html_safe if content
if method.present? && options[:as] ||= default_input_type(method)
input(method, options)
end
end

protected

# Returns the default filter type for a given attribute
def default_input_type(method, options = {})
if (column = column_for(method))
if reflection_for(method) || polymorphic_foreign_type?(method)
:select
elsif column = column_for(method)
case column.type
when :date, :datetime
return :date_range
:date_range
when :string, :text
return :string
when :integer
return :select if reflection_for(method.to_s.gsub('_id','').to_sym)
return :numeric
when :float, :decimal
return :numeric
:string
when :integer, :float, :decimal
:numeric
when :boolean
return :boolean
:boolean
end
end

if (reflection = reflection_for(method))
return :select if reflection.macro == :belongs_to && !reflection.options[:polymorphic]
end
end

def custom_input_class_name(as)
Expand All @@ -45,17 +45,6 @@ def active_admin_input_class_name(as)
"ActiveAdmin::Inputs::Filter#{as.to_s.camelize}Input"
end

# Returns the column for an attribute on the object being searched
# if it exists. Otherwise returns nil
def column_for(method)
@object.base.columns_hash[method.to_s] if @object.base.respond_to?(:columns_hash)
end

# Returns the association reflection for the method if it exists
def reflection_for(method)
@object.base.reflect_on_association(method) if @object.base.respond_to?(:reflect_on_association)
end

end


Expand All @@ -64,26 +53,27 @@ module ViewHelper

# Helper method to render a filter form
def active_admin_filters_form_for(search, filters, options = {})
options[:builder] ||= ActiveAdmin::Filters::FormBuilder
options[:url] ||= collection_path
options[:html] ||= {}
options[:html][:method] = :get
options[:html][:class] ||= "filter_form"
options[:as] = :q
clear_link = link_to(I18n.t('active_admin.clear_filters'), "#", :class => "clear_filters_btn")
defaults = { :builder => ActiveAdmin::Filters::FormBuilder,
:url => collection_path,
:html => {:class => 'filter_form'} }
required = { :html => {:method => :get},
:as => :q }
options = defaults.deep_merge(options).deep_merge(required)

form_for search, options do |f|
filters.group_by{ |o| o[:attribute] }.each do |attribute, array|
options = array.last # grab last-defined `filter` call from DSL
if_block = options[:if] || proc{ true }
unless_block = options[:unless] || proc{ false }
if call_method_or_proc_on(self, if_block) && !call_method_or_proc_on(self, unless_block)
f.filter options[:attribute], options.except(:attribute, :if, :unless)
opts = array.last # grab last-defined `filter` call from DSL
should = opts.delete(:if) || proc{ true }
shouldnt = opts.delete(:unless) || proc{ false }

if call_method_or_proc_on(self, should) && !call_method_or_proc_on(self, shouldnt)
f.filter attribute, opts
end
end

buttons = content_tag :div, :class => "buttons" do
f.submit(I18n.t('active_admin.filter')) +
clear_link +
link_to(I18n.t('active_admin.clear_filters'), "#", :class => "clear_filters_btn") +
hidden_field_tags_for(params, :except => [:q, :page])
end

Expand Down
45 changes: 45 additions & 0 deletions lib/active_admin/filters/formtastic_addons.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
module ActiveAdmin
module Filters
module FormtasticAddons

#
# The below are Formtastic overrides to use `base` instead of `class` for MetaSearch.
#

# Returns the default label for a given attribute. Uses ActiveModel I18n if available.
def humanized_method_name
if object.base.respond_to?(:human_attribute_name)
object.base.human_attribute_name(method)
else
method.to_s.send(builder.label_str_method)
end
end

# Returns the association reflection for the method if it exists
def reflection_for(method)
@object.base.reflect_on_association(method) if @object.base.respond_to?(:reflect_on_association)
end

# Returns the column for an attribute on the object being searched if it exists.
def column_for(method)
@object.base.columns_hash[method.to_s] if @object.base.respond_to?(:columns_hash)
end

#
# The below are custom methods that Formtastic does not provide.
#

def foreign_key?(method)
@object.base.reflections.select{ |_,r| r.macro == :belongs_to }.values
.map(&:foreign_key).include? method.to_s
end

def polymorphic_foreign_type?(method)
type = Rails::VERSION::MAJOR == 3 && Rails::VERSION::MINOR == 0 ? proc{ |r| r.options[:foreign_type] } : :foreign_type
@object.base.reflections.values.select{ |r| r.macro == :belongs_to && r.options[:polymorphic] }
.map(&type).include? method.to_s
end

end
end
end
10 changes: 9 additions & 1 deletion lib/active_admin/filters/resource_extension.rb
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,15 @@ def default_filters
# Returns a default set of filters for the associations
def default_association_filters
if resource_class.respond_to?(:reflections)
resource_class.reflections.collect{|name, r| { :attribute => name }}
block = if Rails::VERSION::MAJOR == 3 && Rails::VERSION::MINOR == 0
proc{ |_,r| [r.options[:foreign_type], r.primary_key_name] }
else
proc{ |_,r| [r.foreign_type, r.foreign_key] }
end

poly, not_poly = resource_class.reflections.partition{ |_,r| r.macro == :belongs_to && r.options[:polymorphic] }
filters = poly.map(&block).flatten + not_poly.map(&:first)
filters.collect{ |name| { :attribute => name.to_sym } }
else
[]
end
Expand Down
4 changes: 2 additions & 2 deletions lib/active_admin/form_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ def initialize(*args)
end

def inputs(*args, &block)
@inputs_with_block = block_given?
@use_form_buffer = block_given?
form_buffers.last << with_new_form_buffer{ super }
end

# If this `input` call is inside a `inputs` block, add the content
# to the form buffer. Else, return it directly.
def input(method, *args)
content = with_new_form_buffer{ super }
@inputs_with_block ? form_buffers.last << content : content
@use_form_buffer ? form_buffers.last << content : content
end

def cancel_link(url = {:action => "index"}, html_options = {}, li_attrs = {})
Expand Down
19 changes: 2 additions & 17 deletions lib/active_admin/inputs/filter_base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@ module ActiveAdmin
module Inputs
module FilterBase
include ::Formtastic::Inputs::Base
include ::ActiveAdmin::Filters::FormtasticAddons

extend ::ActiveSupport::Autoload
autoload :SearchMethodSelect

def input_wrapping(&block)
template.content_tag :div,
template.capture(&block),
wrapper_html_options
template.content_tag :div, template.capture(&block), wrapper_html_options
end

def required?
Expand All @@ -29,20 +28,6 @@ def collection_from_options
end
end

# Returns the default label for a given attribute
# Will use ActiveModel I18n if possible
def humanized_method_name
if object.base.respond_to?(:human_attribute_name)
object.base.human_attribute_name(method)
else
method.to_s.send(builder.label_str_method)
end
end

# Returns the association reflection for the method if it exists
def reflection_for(method)
@object.base.reflect_on_association(method) if @object.base.respond_to?(:reflect_on_association)
end
end
end
end
33 changes: 23 additions & 10 deletions lib/active_admin/inputs/filter_select_input.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,38 @@ module Inputs
class FilterSelectInput < ::Formtastic::Inputs::SelectInput
include FilterBase

# When it's a HABTM or has_many association, Formtastic builds "object_ids".
# Metasearch requires "objects_id", hence the convoluted override.
#
# We use "_in" instead of "_eq" since it works for single or multiple values.
def input_name
"#{super}_eq"
reflection ? "#{method}_id_in" : "#{method}_in"
end

# Include the "Any" option if it's a dropdown, but not if it's a multi-select.
def input_options
super.merge(:include_blank => I18n.t('active_admin.any'))
super.merge :include_blank => multiple? ? false : I18n.t('active_admin.any')
end

def method
if super.to_s.scan(/_id/).count('_id') == 1
super.to_s.sub(/_id$/, '').to_sym
else
super.to_s.to_sym
end
# was "#{object_name}[#{association_primary_key}]"
def input_html_options_name
"#{object_name}[#{input_name}]"
end

def extra_input_html_options
{}
# Would normally return true for has_many and HABTM, which would subsequently
# cause the select field to be multi-select instead of a dropdown.
def multiple_by_association?
false
end

# Provides an efficient default lookup query if the attribute is a DB column.
def collection
unless Rails::VERSION::MAJOR == 3 && Rails::VERSION::MINOR < 2
return @object.base.uniq.pluck method if !options[:collection] && column_for(method)
end
super
end

end
end
end
28 changes: 13 additions & 15 deletions spec/unit/filters/filter_form_builder_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ def filter(name, options = {})
end
end

describe "belong to" do
describe "belongs_to" do
before do
@john = User.create :first_name => "John", :last_name => "Doe", :username => "john_doe"
@jane = User.create :first_name => "Jane", :last_name => "Doe", :username => "jane_doe"
Expand All @@ -177,21 +177,17 @@ def filter(name, options = {})
context "when given as the _id attribute name" do
let(:body) { filter :author_id }

it "should not render as an integer" do
body.should_not have_tag "input", :attributes => { :name => "q[author_id_eq]" }
end
it "should render as belongs to select" do
body.should have_tag "select", :attributes => { :name => "q[author_id_eq]" }
body.should have_tag "option", "John Doe", :attributes => { :value => @john.id }
body.should have_tag "option", "Jane Doe", :attributes => { :value => @jane.id }
it "should generate a numeric filter" do
body.should have_tag "label", :attributes => { :for => "author_id_numeric" }
body.should have_tag "input", :attributes => { :id => "author_id_numeric" }
end
end

context "when given as the name of the relationship" do
let(:body) { filter :author }

it "should generate a select" do
body.should have_tag "select", :attributes => { :name => "q[author_id_eq]" }
body.should have_tag "select", :attributes => { :name => "q[author_id_in]" }
end
it "should set the default text to 'Any'" do
body.should have_tag "option", "Any", :attributes => { :value => "" }
Expand Down Expand Up @@ -234,16 +230,18 @@ def filter(name, options = {})
end

context "when polymorphic relationship" do
let(:body) do
search = ActiveAdmin::Comment.search
render_filter(search, [{:attribute => :resource}])
end
it "should not generate any field" do
body.should have_tag("form", :attributes => { :method => 'get' })
it "should raise an error if a collection isn't provided" do
expect {
search = ActiveAdmin::Comment.search
render_filter(search, [{:attribute => :resource}])
}.to raise_error Formtastic::PolymorphicInputWithoutCollectionError
end
end
end # belongs to

describe "has_and_belongs_to_many" do
pending "add HABTM models so this can be mocked out"
end

describe "conditional display" do

Expand Down

0 comments on commit fe5becd

Please sign in to comment.