datagrid 0.9.3 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/Readme.markdown +6 -4
  2. data/VERSION +1 -1
  3. data/app/assets/stylesheets/datagrid.css.sass +132 -0
  4. data/app/views/datagrid/_form.html.erb +5 -2
  5. data/app/views/datagrid/_order_for.html.erb +2 -2
  6. data/app/views/datagrid/_table.html.erb +1 -1
  7. data/datagrid.gemspec +10 -3
  8. data/lib/datagrid.rb +1 -0
  9. data/lib/datagrid/column_names_attribute.rb +38 -7
  10. data/lib/datagrid/columns.rb +38 -4
  11. data/lib/datagrid/columns/column.rb +29 -1
  12. data/lib/datagrid/drivers/abstract_driver.rb +8 -0
  13. data/lib/datagrid/drivers/active_record.rb +29 -1
  14. data/lib/datagrid/drivers/array.rb +14 -2
  15. data/lib/datagrid/drivers/mongo_mapper.rb +8 -0
  16. data/lib/datagrid/drivers/mongoid.rb +9 -1
  17. data/lib/datagrid/filters.rb +24 -6
  18. data/lib/datagrid/filters/base_filter.rb +42 -14
  19. data/lib/datagrid/filters/boolean_enum_filter.rb +1 -1
  20. data/lib/datagrid/filters/dynamic_filter.rb +57 -0
  21. data/lib/datagrid/filters/enum_filter.rb +4 -21
  22. data/lib/datagrid/filters/select_options.rb +26 -0
  23. data/lib/datagrid/form_builder.rb +41 -8
  24. data/lib/datagrid/helper.rb +2 -1
  25. data/lib/datagrid/i18n.rb +0 -0
  26. data/lib/datagrid/locale/en.yml +28 -0
  27. data/lib/datagrid/ordering.rb +33 -19
  28. data/lib/datagrid/utils.rb +8 -9
  29. data/spec/datagrid/column_names_attribute_spec.rb +44 -1
  30. data/spec/datagrid/columns_spec.rb +16 -0
  31. data/spec/datagrid/filters/dynamic_filter_spec.rb +37 -0
  32. data/spec/datagrid/filters/integer_filter_spec.rb +18 -0
  33. data/spec/datagrid/filters/string_filter_spec.rb +25 -0
  34. data/spec/datagrid/filters_spec.rb +15 -1
  35. data/spec/datagrid/form_builder_spec.rb +83 -0
  36. data/spec/datagrid/helper_spec.rb +1 -0
  37. data/spec/datagrid/ordering_spec.rb +41 -1
  38. data/spec/datagrid/utils_spec.rb +7 -2
  39. metadata +11 -4
@@ -66,6 +66,14 @@ module Datagrid
66
66
  raise NotImplementedError
67
67
  end
68
68
 
69
+ def contains(scope, field, value)
70
+ raise NotImplementedError
71
+ end
72
+
73
+ def column_names(scope)
74
+ raise NotImplementedError
75
+ end
76
+
69
77
  protected
70
78
  def timestamp_class?(klass)
71
79
  TIMESTAMP_CLASSES.include?(klass)
@@ -40,6 +40,10 @@ module Datagrid
40
40
  scope.reorder(order).reverse_order
41
41
  end
42
42
 
43
+ def reverse_order(scope)
44
+ scope.reverse_order
45
+ end
46
+
43
47
  def default_order(scope, column_name)
44
48
  has_column?(scope, column_name) ? [scope.table_name, column_name].join(".") : nil
45
49
  end
@@ -58,8 +62,22 @@ module Datagrid
58
62
  false
59
63
  end
60
64
 
65
+ def column_names(scope)
66
+ scope.column_names
67
+ end
68
+
61
69
  def is_timestamp?(scope, field)
62
- has_column?(scope, field) && scope.columns_hash[field.to_s].type == :datetime
70
+ column_type(scope, field) == :datetime
71
+ end
72
+
73
+ def contains(scope, field, value)
74
+ if column_type(scope, field) == :string
75
+ field = prefix_table_name(scope, field)
76
+ scope.where("#{field} #{contains_predicate} ?", "%#{value}%")
77
+ else
78
+ # dont support contains operation by non-varchar column now
79
+ scope.where("1=0")
80
+ end
63
81
  end
64
82
 
65
83
  protected
@@ -67,6 +85,16 @@ module Datagrid
67
85
  def prefix_table_name(scope, field)
68
86
  has_column?(scope, field) ? [scope.table_name, field].join(".") : field
69
87
  end
88
+
89
+ def contains_predicate
90
+ defined?(::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter) &&
91
+ ::ActiveRecord::Base.connection.is_a?(::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter) ?
92
+ 'ilike' : 'like'
93
+ end
94
+
95
+ def column_type(scope, field)
96
+ has_column?(scope, field) ? scope.columns_hash[field.to_s].type : nil
97
+ end
70
98
  end
71
99
  end
72
100
  end
@@ -34,13 +34,15 @@ module Datagrid
34
34
 
35
35
  def greater_equal(scope, field, value)
36
36
  scope.select do |object|
37
- object.send(field) >= value
37
+ compare_value = object.send(field)
38
+ compare_value.respond_to?(:>=) && compare_value >= value
38
39
  end
39
40
  end
40
41
 
41
42
  def less_equal(scope, field, value)
42
43
  scope.select do |object|
43
- object.send(field) <= value
44
+ compare_value = object.send(field)
45
+ compare_value.respond_to?(:<=) && compare_value <= value
44
46
  end
45
47
  end
46
48
 
@@ -52,6 +54,16 @@ module Datagrid
52
54
  has_column?(scope, column_name) &&
53
55
  timestamp_class?(scope.first.send(column_name).class)
54
56
  end
57
+
58
+ def contains(scope, field, value)
59
+ scope.select do |object|
60
+ object.send(field).to_s.include?(value)
61
+ end
62
+ end
63
+
64
+ def column_names(scope)
65
+ []
66
+ end
55
67
  end
56
68
  end
57
69
  end
@@ -47,6 +47,14 @@ module Datagrid
47
47
  #TODO implement the support
48
48
  false
49
49
  end
50
+
51
+ def contains(scope, field, value)
52
+ scope(field => Regexp.compile(Regexp.escape(value)))
53
+ end
54
+
55
+ def column_names(scope)
56
+ [] # TODO: implement support
57
+ end
50
58
  end
51
59
  end
52
60
  end
@@ -43,13 +43,21 @@ module Datagrid
43
43
  end
44
44
 
45
45
  def has_column?(scope, column_name)
46
- to_scope(scope).klass.fields.keys.include?(column_name.to_s)
46
+ column_names(scope).include?(column_name.to_s)
47
47
  end
48
48
 
49
49
  def is_timestamp?(scope, column_name)
50
50
  has_column?(scope, column_name) &&
51
51
  timestamp_class?(to_scope(scope).klass.fields[column_name.to_s].type)
52
52
  end
53
+
54
+ def contains(scope, field, value)
55
+ scope(field => Regexp.compile(Regexp.escape(value)))
56
+ end
57
+
58
+ def column_names(scope)
59
+ to_scope(scope).klass.fields.keys
60
+ end
53
61
  end
54
62
  end
55
63
  end
@@ -13,6 +13,7 @@ module Datagrid
13
13
  require "datagrid/filters/composite_filters"
14
14
  require "datagrid/filters/string_filter"
15
15
  require "datagrid/filters/float_filter"
16
+ require "datagrid/filters/dynamic_filter"
16
17
 
17
18
  FILTER_TYPES = {
18
19
  :date => Filters::DateFilter,
@@ -23,6 +24,7 @@ module Datagrid
23
24
  :integer => Filters::IntegerFilter,
24
25
  :enum => Filters::EnumFilter,
25
26
  :float => Filters::FloatFilter,
27
+ :dynamic => Filters::DynamicFilter
26
28
  }
27
29
 
28
30
  def self.included(base) #:nodoc:
@@ -72,7 +74,7 @@ module Datagrid
72
74
  # * <tt>:dummy</tt> - if true, this filter will not be applied automatically
73
75
  # and will just be displayed in form. In case you may want to apply it manually.
74
76
  #
75
- # See: https://github.com/bogdan/datagrid/wiki/Columns for examples
77
+ # See: https://github.com/bogdan/datagrid/wiki/Filters for examples
76
78
  def filter(name, type = :default, options = {}, &block)
77
79
  if type.is_a?(Hash)
78
80
  options = type
@@ -111,11 +113,7 @@ module Datagrid
111
113
  end
112
114
 
113
115
  def assets # :nodoc:
114
- result = super
115
- self.class.filters.each do |filter|
116
- result = filter.apply(self, result, filter_value(filter))
117
- end
118
- result
116
+ apply_filters(super, self.class.filters)
119
117
  end
120
118
 
121
119
  # Returns all defined filters Array
@@ -128,11 +126,31 @@ module Datagrid
128
126
  self[filter.name]
129
127
  end
130
128
 
129
+ # Returns string representation of filter value
130
+ def filter_value_as_string(filter)
131
+ value = filter_value(filter)
132
+ value = value.is_a?(Array) ? value.join(filter.separator) : value.to_s
133
+ value.blank? ? nil : value
134
+ end
135
+
131
136
  # Returns filter object with the given name
132
137
  def filter_by_name(name)
133
138
  self.class.filter_by_name(name)
134
139
  end
135
140
 
141
+ # Returns assets filtered only by specified filters
142
+ # Allows partial filtering
143
+ def filter_by(*filters)
144
+ apply_filters(scope, filters.map{|f| filter_by_name(f)})
145
+ end
146
+
147
+ protected
148
+
149
+ def apply_filters(current_scope, filters)
150
+ filters.inject(current_scope) do |result, filter|
151
+ filter.apply(self, result, filter_value(filter))
152
+ end
153
+ end
136
154
  end # InstanceMethods
137
155
 
138
156
  end
@@ -16,14 +16,15 @@ class Datagrid::Filters::BaseFilter
16
16
  raise NotImplementedError, "#parse(value) suppose to be overwritten"
17
17
  end
18
18
 
19
+
20
+ def unapplicable_value?(value)
21
+ value.nil? ? !allow_nil? : value.blank? && !allow_blank?
22
+ end
23
+
19
24
  def apply(grid_object, scope, value)
20
- if value.nil?
21
- return scope if !allow_nil?
22
- else
23
- return scope if value.blank? && !allow_blank?
24
- end
25
+ return scope if unapplicable_value?(value)
25
26
 
26
- result = execute(value, scope, grid_object, &block)
27
+ result = execute(value, scope, grid_object)
27
28
  return scope unless result
28
29
  unless grid_object.driver.match?(result)
29
30
  raise Datagrid::FilteringError, "Can not apply #{name.inspect} filter: result #{result.inspect} no longer match #{grid_object.driver.class}."
@@ -32,14 +33,20 @@ class Datagrid::Filters::BaseFilter
32
33
  end
33
34
 
34
35
  def parse_values(value)
35
- if !self.multiple && value.is_a?(Array)
36
- raise Datagrid::ArgumentError, "#{grid_class}##{name} filter can not accept Array argument. Use :multiple option."
37
- end
38
- values = Array.wrap(value)
39
- values.map! do |v|
40
- self.parse(v)
36
+ if multiple?
37
+ normalize_multiple_value(value).map do |v|
38
+ parse(v)
39
+ end
40
+ else
41
+ if value.is_a?(Array)
42
+ raise Datagrid::ArgumentError, "#{grid_class}##{name} filter can not accept Array argument. Use :multiple option."
43
+ end
44
+ parse(value)
41
45
  end
42
- self.multiple ? values : values.first
46
+ end
47
+
48
+ def separator
49
+ options[:multiple].is_a?(String) ? options[:multiple] : default_separator
43
50
  end
44
51
 
45
52
  def header
@@ -53,6 +60,11 @@ class Datagrid::Filters::BaseFilter
53
60
  end
54
61
 
55
62
  def multiple
63
+ Datagrid::Utils.warn_once("Filter#multiple method is deprecated. Use Filter#multiple? instead")
64
+ multiple?
65
+ end
66
+
67
+ def multiple?
56
68
  self.options[:multiple]
57
69
  end
58
70
 
@@ -102,7 +114,7 @@ class Datagrid::Filters::BaseFilter
102
114
  driver.where(scope, name, value)
103
115
  end
104
116
 
105
- def execute(value, scope, grid_object, &block)
117
+ def execute(value, scope, grid_object)
106
118
  if block.arity == 1
107
119
  scope.instance_exec(value, &block)
108
120
  else
@@ -110,5 +122,21 @@ class Datagrid::Filters::BaseFilter
110
122
  end
111
123
  end
112
124
 
125
+ def normalize_multiple_value(value)
126
+ case value
127
+ when String
128
+ #TODO: write tests and doc
129
+ value.split(separator)
130
+ when Array
131
+ value
132
+ else
133
+ Array.wrap(value)
134
+ end
135
+ end
136
+
137
+ def default_separator
138
+ ','
139
+ end
140
+
113
141
  end
114
142
 
@@ -5,7 +5,7 @@ class Datagrid::Filters::BooleanEnumFilter < Datagrid::Filters::EnumFilter
5
5
 
6
6
  def initialize(report, attribute, options = {}, &block)
7
7
  options[:select] = [YES, NO].map do |key, value|
8
- [I18n.t("datagrid.filters.eboolean.#{key.downcase}", :default => key.humanize), key]
8
+ [I18n.t("datagrid.filters.eboolean.#{key.downcase}"), key]
9
9
  end
10
10
  super(report, attribute, options, &block)
11
11
  end
@@ -0,0 +1,57 @@
1
+ require "datagrid/filters/select_options"
2
+
3
+ class Datagrid::Filters::DynamicFilter < Datagrid::Filters::BaseFilter
4
+
5
+ include Datagrid::Filters::SelectOptions
6
+
7
+ def initialize(*)
8
+ super
9
+ options[:multiple] = true
10
+ options[:select] ||= default_select
11
+ end
12
+
13
+ def parse(value)
14
+ value
15
+ end
16
+
17
+ def unapplicable_value?(filter)
18
+ field, operation, value = filter
19
+ field.blank? || operation.blank? || super(value)
20
+ end
21
+
22
+ def default_filter_where(driver, scope, filter)
23
+ field, operation, value = filter
24
+ driver.to_scope(scope)
25
+ case operation
26
+ when '='
27
+ driver.where(scope, field, value)
28
+ when '=~'
29
+ driver.contains(scope, field, value)
30
+ when '>='
31
+ driver.greater_equal(scope, field, value)
32
+ when '<='
33
+ driver.less_equal(scope, field, value)
34
+ else
35
+ raise "unknown operation: #{operation.inspect}"
36
+ end
37
+ end
38
+
39
+ def operations_select
40
+ %w(= =~ >= <=).map do |operation|
41
+ I18n.t(operation, :scope => "datagrid.filters.dynamic.operations")
42
+ end
43
+ end
44
+
45
+ protected
46
+
47
+ def default_select
48
+ proc {|grid|
49
+ grid.driver.column_names(grid.scope).map do |name|
50
+ # Mongodb/Rails problem:
51
+ # '_id'.humanize returns ''
52
+ [name.gsub(/^_/, '').humanize.strip, name]
53
+ end
54
+ }
55
+ end
56
+
57
+ end
@@ -1,5 +1,9 @@
1
+ require "datagrid/filters/select_options"
2
+
1
3
  class Datagrid::Filters::EnumFilter < Datagrid::Filters::BaseFilter
2
4
 
5
+ include Datagrid::Filters::SelectOptions
6
+
3
7
  def initialize(*args)
4
8
  super(*args)
5
9
  raise Datagrid::ConfigurationError, ":select option not specified" unless options[:select]
@@ -10,27 +14,6 @@ class Datagrid::Filters::EnumFilter < Datagrid::Filters::BaseFilter
10
14
  value
11
15
  end
12
16
 
13
- def select(object = nil)
14
- select = self.options[:select]
15
- if select.is_a?(Symbol)
16
- object.send(select)
17
- elsif select.respond_to?(:call)
18
- Datagrid::Utils.apply_args(object, &select)
19
- else
20
- select
21
- end
22
- end
23
-
24
- def include_blank
25
- unless self.prompt
26
- self.options.has_key?(:include_blank) ? options[:include_blank] : !multiple
27
- end
28
- end
29
-
30
- def prompt
31
- self.options.has_key?(:prompt) ? options[:prompt] : false
32
- end
33
-
34
17
  def strict
35
18
  self.options[:strict]
36
19
  end
@@ -0,0 +1,26 @@
1
+ module Datagrid::Filters::SelectOptions
2
+
3
+ def select(object = nil)
4
+ #unless object
5
+ #Datagrid::Utils.warn_once("#{self.class.name}#select without argument is deprecated")
6
+ #end
7
+ select = self.options[:select]
8
+ if select.is_a?(Symbol)
9
+ object.send(select)
10
+ elsif select.respond_to?(:call)
11
+ Datagrid::Utils.apply_args(object, &select)
12
+ else
13
+ select
14
+ end
15
+ end
16
+
17
+ def include_blank
18
+ unless prompt
19
+ options.has_key?(:include_blank) ? options[:include_blank] : !multiple?
20
+ end
21
+ end
22
+
23
+ def prompt
24
+ options.has_key?(:prompt) ? options[:prompt] : false
25
+ end
26
+ end
@@ -3,12 +3,14 @@ require "action_view"
3
3
  module Datagrid
4
4
  module FormBuilder
5
5
 
6
+ # Returns a form input html for the corresponding filter name
6
7
  def datagrid_filter(filter_or_attribute, options = {})
7
8
  filter = datagrid_get_filter(filter_or_attribute)
8
- options = Datagrid::Utils.add_html_classes(options, filter.name, datagrid_filter_html_class(filter))
9
+ options = add_html_classes(options, filter.name, datagrid_filter_html_class(filter))
9
10
  self.send(filter.form_builder_helper_name, filter, options)
10
11
  end
11
12
 
13
+ # Returns a form label html for the corresponding filter name
12
14
  def datagrid_label(filter_or_attribute, options = {})
13
15
  filter = datagrid_get_filter(filter_or_attribute)
14
16
  self.label(filter.name, filter.header, options)
@@ -28,25 +30,52 @@ module Datagrid
28
30
  end
29
31
 
30
32
  def datagrid_default_filter(attribute_or_filter, options = {})
31
- text_field datagrid_get_attribute(attribute_or_filter), options
33
+ filter = datagrid_get_filter(attribute_or_filter)
34
+ value = object.filter_value_as_string(filter)
35
+ text_field filter.name, options.merge(:value => object.filter_value_as_string(filter))
32
36
  end
33
37
 
34
38
  def datagrid_enum_filter(attribute_or_filter, options = {})
35
39
  filter = datagrid_get_filter(attribute_or_filter)
36
- if !options.has_key?(:multiple) && filter.multiple
40
+ if !options.has_key?(:multiple) && filter.multiple?
37
41
  options[:multiple] = true
38
42
  end
39
- select filter.name, filter.select(object) || [], {:include_blank => filter.include_blank, :prompt => filter.prompt}, options
43
+ select filter.name, filter.select(object) || [], {:include_blank => filter.include_blank, :prompt => filter.prompt, :include_hidden => false}, options
40
44
  end
41
45
 
42
46
  def datagrid_integer_filter(attribute_or_filter, options = {})
43
47
  filter = datagrid_get_filter(attribute_or_filter)
44
- if filter.multiple && self.object[filter.name].blank?
48
+ if filter.multiple? && self.object[filter.name].blank?
45
49
  options[:value] = ""
46
50
  end
47
51
  datagrid_range_filter(:integer, filter, options)
48
52
  end
49
53
 
54
+ def datagrid_dynamic_filter(attribute_or_filter, options = {})
55
+ filter = datagrid_get_filter(attribute_or_filter)
56
+ input_name = "#{object_name}[#{filter.name.to_s}][]"
57
+ field, operation, value = object.filter_value(filter)
58
+ options = options.merge(:name => input_name)
59
+ field_input = select(
60
+ filter.name,
61
+ filter.select(object) || [],
62
+ {
63
+ :include_blank => filter.include_blank,
64
+ :prompt => filter.prompt,
65
+ :include_hidden => false,
66
+ :selected => field
67
+ },
68
+ add_html_classes(options, "field")
69
+ )
70
+ operation_input = select(
71
+ filter.name, filter.operations_select,
72
+ {:include_blank => false, :include_hidden => false, :prompt => false, :selected => operation },
73
+ add_html_classes(options, "operation")
74
+ )
75
+ value_input = text_field(filter.name, add_html_classes(options, "value").merge(:value => value))
76
+ [field_input, operation_input, value_input].join("\n").html_safe
77
+ end
78
+
50
79
  def datagrid_range_filter(type, attribute_or_filter, options = {})
51
80
  filter = datagrid_get_filter(attribute_or_filter)
52
81
  if filter.range?
@@ -58,18 +87,18 @@ module Datagrid
58
87
  # 2 inputs: "from date" and "to date" to specify a range
59
88
  [
60
89
  text_field(filter.name, from_options),
61
- I18n.t("datagrid.misc.#{type}_range_separator", :default => "<span class=\"separator #{type}\"> - </span>"),
90
+ I18n.t("datagrid.filters.#{type}.range_separator"),
62
91
  text_field(filter.name, to_options)
63
92
  ].join.html_safe
64
93
  else
65
- text_field(filter.name, options)
94
+ datagrid_default_filter(filter, options)
66
95
  end
67
96
  end
68
97
 
69
98
 
70
99
  def datagrid_range_filter_options(object, filter, type, options)
71
100
  type_method_map = {:from => :first, :to => :last}
72
- options = Datagrid::Utils.add_html_classes(options, type)
101
+ options = add_html_classes(options, type)
73
102
  options[:value] = filter.format(object[filter.name].try(type_method_map[type]))
74
103
  # In case of datagrid ranged filter
75
104
  # from and to input will have same id
@@ -110,6 +139,10 @@ module Datagrid
110
139
  filter.class.to_s.demodulize.underscore
111
140
  end
112
141
 
142
+ def add_html_classes(options, *classes)
143
+ Datagrid::Utils.add_html_classes(options, *classes)
144
+ end
145
+
113
146
  class Error < StandardError
114
147
  end
115
148
  end