datagrid 0.9.3 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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