Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Formtastic 2.0 support #527

Closed
wants to merge 5 commits into from
Closed
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
2 changes: 1 addition & 1 deletion activeadmin.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Gem::Specification.new do |s|
s.add_dependency("rails", ">= 3.0.0")
s.add_dependency("meta_search", ">= 0.9.2")
s.add_dependency("devise", ">= 1.1.2")
s.add_dependency("formtastic", "< 2.0.0")
s.add_dependency("formtastic", ">= 2.0.0")
s.add_dependency("inherited_resources", "< 1.3.0")
s.add_dependency("kaminari", ">= 0.12.4")
s.add_dependency("sass", ">= 3.1.0")
Expand Down
25 changes: 25 additions & 0 deletions features/index/filter_with_check_boxes.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
Feature: Filter with check boxes

Background:
Given an index configuration of:
"""
ActiveAdmin.register Post do
filter :author, :as => :check_boxes
end
"""
And a post with the title "Hello World" written by "Jane Doe" exists
And 1 post exists
And I am on the index page for posts

Scenario: Filtering posts written by anyone
When I press "Filter"
Then I should see 2 posts in the table
And I should see "Hello World" within ".index_table"
And the "jane_doe" checkbox should not be checked

Scenario: Filtering posts written by Jane Doe
When I check "jane_doe"
And I press "Filter"
Then I should see 1 posts in the table
And I should see "Hello World" within ".index_table"
And the "jane_doe" checkbox should be checked
3 changes: 3 additions & 0 deletions lib/active_admin.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require 'meta_search'
require 'devise'
require 'kaminari'
require 'formtastic'
require 'sass'
require 'active_admin/arbre'
require 'active_admin/engine'
Expand All @@ -22,6 +23,8 @@ module ActiveAdmin
autoload :DSL, 'active_admin/dsl'
autoload :Event, 'active_admin/event'
autoload :FormBuilder, 'active_admin/form_builder'
autoload :FilterFormBuilder, 'active_admin/filter_form_builder'
autoload :Inputs, 'active_admin/inputs'
autoload :Iconic, 'active_admin/iconic'
autoload :Menu, 'active_admin/menu'
autoload :MenuItem, 'active_admin/menu_item'
Expand Down
53 changes: 53 additions & 0 deletions lib/active_admin/filter_form_builder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
module ActiveAdmin
# 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 FilterFormBuilder < FormBuilder

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

protected

# Returns the default filter type for a given attribute
def default_input_type(method, options = {})
if column = column_for(method)
case column.type
when :date, :datetime
return :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
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)
"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
end
44 changes: 25 additions & 19 deletions lib/active_admin/form_builder.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
require 'formtastic'

module ActiveAdmin
class FormBuilder < ::Formtastic::SemanticFormBuilder
class FormBuilder < ::Formtastic::FormBuilder

attr_reader :form_buffers

Expand All @@ -21,8 +19,6 @@ def inputs(*args, &block)
# its contents, so we want to skip the internal buffering
# while building up its contents
def input(method, *args)
return if polymorphic_belongs_to_association?(method)

content = with_new_form_buffer { super }
return content.html_safe unless @inputs_with_block
form_buffers.last << content.html_safe
Expand Down Expand Up @@ -52,14 +48,6 @@ def commit_button_with_cancel_link
content << cancel_link
end

def datepicker_input(method, options)
options = options.dup
options[:input_html] ||= {}
options[:input_html][:class] = [options[:input_html][:class], "datepicker"].compact.join(' ')
options[:input_html][:size] ||= "10"
string_input(method, options)
end

def has_many(association, options = {}, &block)
options = { :for => association }.merge(options)
options[:class] ||= ""
Expand Down Expand Up @@ -98,15 +86,33 @@ def has_many(association, options = {}, &block)
form_buffers.last << content.html_safe
end

private
protected

# Pass in a method to check if it's a polymorphic association
def polymorphic_belongs_to_association?(method)
reflection = reflection_for(method)

reflection && reflection.macro == :belongs_to && reflection.options[:polymorphic]
def active_admin_input_class_name(as)
"ActiveAdmin::Inputs::#{as.to_s.camelize}Input"
end

def input_class(as)
@input_classes_cache ||= {}
@input_classes_cache[as] ||= begin
begin
begin
custom_input_class_name(as).constantize
rescue NameError
begin
active_admin_input_class_name(as).constantize
rescue NameError
standard_input_class_name(as).constantize
end
end
rescue NameError
raise Formtastic::UnknownInputError
end
end
end

private

def with_new_form_buffer
form_buffers << "".html_safe
return_value = yield
Expand Down
14 changes: 14 additions & 0 deletions lib/active_admin/inputs.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module ActiveAdmin
module Inputs
extend ActiveSupport::Autoload

autoload :DatepickerInput

autoload :FilterBase
autoload :FilterStringInput
autoload :FilterDateRangeInput
autoload :FilterNumericInput
autoload :FilterSelectInput
autoload :FilterCheckBoxesInput
end
end
11 changes: 11 additions & 0 deletions lib/active_admin/inputs/datepicker_input.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module ActiveAdmin
module Inputs
class DatepickerInput < ::Formtastic::Inputs::StringInput
def input_html_options
options = super
options[:class] = [options[:class], "datepicker"].compact.join(' ')
options
end
end
end
end
46 changes: 46 additions & 0 deletions lib/active_admin/inputs/filter_base.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
module ActiveAdmin
module Inputs
module FilterBase
include ::Formtastic::Inputs::Base

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

def required?
false
end

def wrapper_html_options
{ :class => "filter_form_field #{as}" }
end

# Override the standard finder to accept a proc
def collection_from_options
if options[:collection].is_a?(Proc)
options[:collection].call
else
super
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
40 changes: 40 additions & 0 deletions lib/active_admin/inputs/filter_check_boxes_input.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
module ActiveAdmin
module Inputs
class FilterCheckBoxesInput < ::Formtastic::Inputs::CheckBoxesInput
include FilterBase

def input_name
"#{object_name}[#{association_primary_key || method}_in][]"
end

def selected_values
@object.send("#{association_primary_key || method}_in") || []
end

# Add whitespace before label
def choice_label(choice)
" #{super(choice)}"
end

# Don't wrap in UL tag
def choices_group_wrapping(&block)
template.capture(&block)
end

# Don't wrap in LI tag
def choice_wrapping(html_options, &block)
template.capture(&block)
end

# Don't render hidden fields
def hidden_field_for_all
""
end

# Don't render hidden fields
def hidden_fields?
false
end
end
end
end
34 changes: 34 additions & 0 deletions lib/active_admin/inputs/filter_date_range_input.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
module ActiveAdmin
module Inputs
class FilterDateRangeInput < ::Formtastic::Inputs::StringInput
include FilterBase

def to_html
input_wrapping do
[ label_html,
builder.text_field(gt_input_name, input_html_options(gt_input_name)),
template.content_tag(:span, "-", :class => "seperator"),
builder.text_field(lt_input_name, input_html_options(lt_input_name)),
].join("\n").html_safe
end
end

def gt_input_name
"#{method}_gte"
end
alias :input_name :gt_input_name

def lt_input_name
"#{method}_lte"
end

def input_html_options(input_name = gt_input_name)
current_value = @object.send(input_name)
{ :size => 12,
:class => "datepicker",
:max => 10,
:value => current_value.respond_to?(:strftime) ? current_value.strftime("%Y-%m-%d") : "" }
end
end
end
end
55 changes: 55 additions & 0 deletions lib/active_admin/inputs/filter_numeric_input.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
module ActiveAdmin
module Inputs
class FilterNumericInput < ::Formtastic::Inputs::NumberInput
include FilterBase

def to_html
input_wrapping do
[ label_html,
select_html,
" ",
input_html
].join("\n").html_safe
end
end

def input_html
builder.text_field current_filter, input_html_options
end

def input_html_options
{ :size => 10, :id => "#{method}_numeric" }
end

def select_html
template.select_tag '', select_options, select_html_options
end

def select_options
template.options_for_select(filters, current_filter)
end

def select_html_options
{ :onchange => "document.getElementById('#{method}_numeric').name = 'q[' + this.value + ']';" }
end

# Returns the scope for which we are currently searching. If no search is available
# it returns the first scope
def current_filter
filters[1..-1].inject(filters.first){|a,b| @object.send(b[1].to_sym) ? b : a }[1]
end

def filters
(options[:filters] || default_filters).collect do |scope|
[scope[0], [method, scope[1]].join("_")]
end
end

def default_filters
[ [I18n.t('active_admin.equal_to'), 'eq'],
[I18n.t('active_admin.greater_than'), 'gt'],
[I18n.t('active_admin.less_than'), 'lt'] ]
end
end
end
end
Loading