datagrid 1.8.1 → 2.0.0.pre.alpha

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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -7
  3. data/{Readme.markdown → README.md} +46 -29
  4. data/app/assets/stylesheets/datagrid.css +145 -0
  5. data/app/views/datagrid/_enum_checkboxes.html.erb +5 -3
  6. data/app/views/datagrid/_form.html.erb +4 -5
  7. data/app/views/datagrid/_head.html.erb +26 -3
  8. data/app/views/datagrid/_range_filter.html.erb +5 -3
  9. data/app/views/datagrid/_row.html.erb +12 -1
  10. data/app/views/datagrid/_table.html.erb +4 -4
  11. data/datagrid.gemspec +8 -8
  12. data/lib/datagrid/active_model.rb +9 -17
  13. data/lib/datagrid/base.rb +39 -0
  14. data/lib/datagrid/column_names_attribute.rb +12 -12
  15. data/lib/datagrid/columns/column.rb +155 -133
  16. data/lib/datagrid/columns.rb +495 -282
  17. data/lib/datagrid/configuration.rb +23 -10
  18. data/lib/datagrid/core.rb +184 -150
  19. data/lib/datagrid/deprecated_object.rb +20 -0
  20. data/lib/datagrid/drivers/abstract_driver.rb +13 -25
  21. data/lib/datagrid/drivers/active_record.rb +24 -26
  22. data/lib/datagrid/drivers/array.rb +26 -17
  23. data/lib/datagrid/drivers/mongo_mapper.rb +15 -14
  24. data/lib/datagrid/drivers/mongoid.rb +16 -18
  25. data/lib/datagrid/drivers/sequel.rb +14 -19
  26. data/lib/datagrid/drivers.rb +2 -1
  27. data/lib/datagrid/engine.rb +11 -3
  28. data/lib/datagrid/filters/base_filter.rb +166 -142
  29. data/lib/datagrid/filters/boolean_filter.rb +19 -5
  30. data/lib/datagrid/filters/date_filter.rb +33 -35
  31. data/lib/datagrid/filters/date_time_filter.rb +24 -16
  32. data/lib/datagrid/filters/default_filter.rb +9 -3
  33. data/lib/datagrid/filters/dynamic_filter.rb +151 -105
  34. data/lib/datagrid/filters/enum_filter.rb +43 -19
  35. data/lib/datagrid/filters/extended_boolean_filter.rb +39 -27
  36. data/lib/datagrid/filters/float_filter.rb +16 -5
  37. data/lib/datagrid/filters/integer_filter.rb +21 -10
  38. data/lib/datagrid/filters/ranged_filter.rb +66 -45
  39. data/lib/datagrid/filters/select_options.rb +58 -49
  40. data/lib/datagrid/filters/string_filter.rb +9 -4
  41. data/lib/datagrid/filters.rb +234 -106
  42. data/lib/datagrid/form_builder.rb +116 -128
  43. data/lib/datagrid/generators/scaffold.rb +185 -0
  44. data/lib/datagrid/generators/views.rb +20 -0
  45. data/lib/datagrid/helper.rb +397 -22
  46. data/lib/datagrid/ordering.rb +81 -87
  47. data/lib/datagrid/rspec.rb +8 -12
  48. data/lib/datagrid/utils.rb +42 -38
  49. data/lib/datagrid/version.rb +3 -1
  50. data/lib/datagrid.rb +18 -28
  51. data/templates/base.rb.erb +33 -7
  52. data/templates/grid.rb.erb +1 -1
  53. metadata +18 -19
  54. data/app/assets/stylesheets/datagrid.sass +0 -134
  55. data/lib/datagrid/filters/composite_filters.rb +0 -49
  56. data/lib/datagrid/renderer.rb +0 -157
  57. data/lib/datagrid/scaffold.rb +0 -129
  58. data/lib/tasks/datagrid_tasks.rake +0 -15
  59. data/templates/controller.rb.erb +0 -6
  60. data/templates/index.html.erb +0 -5
@@ -1,125 +1,171 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "datagrid/filters/select_options"
2
4
 
3
- class Datagrid::Filters::DynamicFilter < Datagrid::Filters::BaseFilter
4
-
5
- include Datagrid::Filters::SelectOptions
6
-
7
- EQUAL_OPERATION = '='
8
- LIKE_OPERATION = '=~'
9
- MORE_EQUAL_OPERATION = '>='
10
- LESS_EQUAL_OPERATION = '<='
11
- DEFAULT_OPERATIONS = [
12
- EQUAL_OPERATION,
13
- LIKE_OPERATION,
14
- MORE_EQUAL_OPERATION,
15
- LESS_EQUAL_OPERATION,
16
- ]
17
- AVAILABLE_OPERATIONS = %w(= =~ >= <=)
18
-
19
- def initialize(*)
20
- super
21
- options[:select] ||= default_select
22
- options[:operations] ||= DEFAULT_OPERATIONS
23
- unless options.has_key?(:include_blank)
24
- options[:include_blank] = false
25
- end
26
- end
5
+ module Datagrid
6
+ module Filters
7
+ class DynamicFilter < Datagrid::Filters::BaseFilter
8
+ include Datagrid::Filters::SelectOptions
27
9
 
28
- def parse_values(filter)
29
- field, operation, value = filter
10
+ EQUAL_OPERATION = "="
11
+ LIKE_OPERATION = "=~"
12
+ MORE_EQUAL_OPERATION = ">="
13
+ LESS_EQUAL_OPERATION = "<="
14
+ DEFAULT_OPERATIONS = [
15
+ EQUAL_OPERATION,
16
+ LIKE_OPERATION,
17
+ MORE_EQUAL_OPERATION,
18
+ LESS_EQUAL_OPERATION,
19
+ ].freeze
20
+ AVAILABLE_OPERATIONS = %w[= =~ >= <=].freeze
30
21
 
31
- [field, operation, type_cast(field, value)]
32
- end
22
+ def initialize(grid, name, **options, &block)
23
+ options[:select] ||= default_select
24
+ options[:operations] ||= DEFAULT_OPERATIONS
25
+ options[:include_blank] = false unless options.key?(:include_blank)
26
+ super
27
+ end
33
28
 
34
- def unapplicable_value?(filter)
35
- _, _, value = filter
36
- super(value)
37
- end
29
+ def default_input_options
30
+ { **super, type: nil }
31
+ end
38
32
 
39
- def default_filter_where(scope, filter)
40
- field, operation, value = filter
41
- date_conversion = value.is_a?(Date) && driver.is_timestamp?(scope, field)
33
+ def parse_values(filter)
34
+ filter ? FilterValue.new(grid_class, filter) : nil
35
+ end
42
36
 
43
- return scope if field.blank? || operation.blank?
44
- unless operations.include?(operation)
45
- raise Datagrid::FilteringError, "Unknown operation: #{operation.inspect}. Available operations: #{operations.join(' ')}"
46
- end
47
- case operation
48
- when EQUAL_OPERATION
49
- if date_conversion
50
- value = Datagrid::Utils.format_date_as_timestamp(value)
37
+ def unapplicable_value?(filter)
38
+ super(filter&.value)
51
39
  end
52
- driver.where(scope, field, value)
53
- when LIKE_OPERATION
54
- if column_type(field) == :string
55
- driver.contains(scope, field, value)
56
- else
57
- if date_conversion
58
- value = Datagrid::Utils.format_date_as_timestamp(value)
40
+
41
+ def default_filter_where(scope, filter)
42
+ field = filter.field
43
+ operation = filter.operation
44
+ value = filter.value
45
+ date_conversion = value.is_a?(Date) && driver.timestamp_column?(scope, field)
46
+
47
+ return scope if field.blank? || operation.blank?
48
+
49
+ unless operations.include?(operation)
50
+ raise Datagrid::FilteringError,
51
+ "Unknown operation: #{operation.inspect}. Available operations: #{operations.join(' ')}"
52
+ end
53
+
54
+ case operation
55
+ when EQUAL_OPERATION
56
+ value = Datagrid::Utils.format_date_as_timestamp(value) if date_conversion
57
+ driver.where(scope, field, value)
58
+ when LIKE_OPERATION
59
+ if column_type(field) == :string
60
+ driver.contains(scope, field, value)
61
+ else
62
+ value = Datagrid::Utils.format_date_as_timestamp(value) if date_conversion
63
+ driver.where(scope, field, value)
64
+ end
65
+ when MORE_EQUAL_OPERATION
66
+ value = value.beginning_of_day if date_conversion
67
+ driver.greater_equal(scope, field, value)
68
+ when LESS_EQUAL_OPERATION
69
+ value = value.end_of_day if date_conversion
70
+ driver.less_equal(scope, field, value)
71
+ else
72
+ raise Datagrid::FilteringError,
73
+ "Unknown operation: #{operation.inspect}. Use filter block argument to implement operation"
59
74
  end
60
- driver.where(scope, field, value)
61
- end
62
- when MORE_EQUAL_OPERATION
63
- if date_conversion
64
- value = value.beginning_of_day
65
75
  end
66
- driver.greater_equal(scope, field, value)
67
- when LESS_EQUAL_OPERATION
68
- if date_conversion
69
- value = value.end_of_day
76
+
77
+ def operations
78
+ options[:operations]
70
79
  end
71
- driver.less_equal(scope, field, value)
72
- else
73
- raise Datagrid::FilteringError, "Unknown operation: #{operation.inspect}. Use filter block argument to implement operation"
74
- end
75
- end
76
80
 
77
- def operations
78
- options[:operations]
79
- end
81
+ def operations_select
82
+ operations.map do |operation|
83
+ [I18n.t(operation, scope: "datagrid.filters.dynamic.operations"), operation]
84
+ end
85
+ end
80
86
 
81
- def operations_select
82
- operations.map do |operation|
83
- [I18n.t(operation, scope: "datagrid.filters.dynamic.operations").html_safe, operation]
84
- end
85
- end
87
+ protected
86
88
 
87
- protected
89
+ def default_select
90
+ proc { |grid|
91
+ grid.driver.column_names(grid.scope).map do |name|
92
+ # Mongodb/Rails problem:
93
+ # '_id'.humanize returns ''
94
+ [name.gsub(%r{^_}, "").humanize.strip, name]
95
+ end
96
+ }
97
+ end
88
98
 
89
- def default_select
90
- proc {|grid|
91
- grid.driver.column_names(grid.scope).map do |name|
92
- # Mongodb/Rails problem:
93
- # '_id'.humanize returns ''
94
- [name.gsub(/^_/, '').humanize.strip, name]
99
+ def column_type(field)
100
+ grid_class.driver.normalized_column_type(grid_class.scope, field)
95
101
  end
96
- }
97
- end
98
102
 
99
- def type_cast(field, value)
100
- type = column_type(field)
101
- return nil if value.blank?
102
- case type
103
- when :string
104
- value.to_s
105
- when :integer
106
- value.is_a?(Numeric) || value =~ /^\d/ ? value.to_i : nil
107
- when :float
108
- value.is_a?(Numeric) || value =~ /^\d/ ? value.to_f : nil
109
- when :date
110
- Datagrid::Utils.parse_date(value)
111
- when :timestamp
112
- Datagrid::Utils.parse_date(value)
113
- when :boolean
114
- Datagrid::Utils.booleanize(value)
115
- when nil
116
- value
117
- else
118
- raise NotImplementedError, "unknown column type: #{type.inspect}"
119
- end
120
- end
103
+ class FilterValue
104
+ attr_accessor :field, :operation, :value
105
+
106
+ def initialize(grid_class, object = nil)
107
+ super()
108
+
109
+ case object
110
+ when Hash
111
+ object = object.symbolize_keys
112
+ self.field = object[:field]
113
+ self.operation = object[:operation]
114
+ self.value = object[:value]
115
+ when Array
116
+ self.field = object[0]
117
+ self.operation = object[1]
118
+ self.value = object[2]
119
+ else
120
+ raise ArgumentError, object.inspect
121
+ end
122
+ return unless grid_class
123
+
124
+ type = grid_class.driver.normalized_column_type(
125
+ grid_class.scope, field,
126
+ )
127
+ self.value = type_cast(type, value)
128
+ end
129
+
130
+ def inspect
131
+ { field: field, operation: operation, value: value }
132
+ end
121
133
 
122
- def column_type(field)
123
- grid_class.driver.normalized_column_type(grid_class.scope, field)
134
+ def to_ary
135
+ to_a
136
+ end
137
+
138
+ def to_a
139
+ [field, operation, value]
140
+ end
141
+
142
+ def to_h
143
+ { field: field, operation: operation, value: value }
144
+ end
145
+
146
+ protected
147
+
148
+ def type_cast(type, value)
149
+ return nil if value.blank?
150
+
151
+ case type
152
+ when :string
153
+ value.to_s
154
+ when :integer
155
+ value.is_a?(Numeric) || value =~ %r{^\d} ? value.to_i : nil
156
+ when :float
157
+ value.is_a?(Numeric) || value =~ %r{^\d} ? value.to_f : nil
158
+ when :date, :timestamp
159
+ Datagrid::Utils.parse_date(value)
160
+ when :boolean
161
+ Datagrid::Utils.booleanize(value)
162
+ when nil
163
+ value
164
+ else
165
+ raise NotImplementedError, "unknown column type: #{type.inspect}"
166
+ end
167
+ end
168
+ end
169
+ end
124
170
  end
125
171
  end
@@ -1,28 +1,52 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "datagrid/filters/select_options"
2
4
 
3
- class Datagrid::Filters::EnumFilter < Datagrid::Filters::BaseFilter
5
+ module Datagrid
6
+ module Filters
7
+ class EnumFilter < Datagrid::Filters::BaseFilter
8
+ include Datagrid::Filters::SelectOptions
4
9
 
5
- include Datagrid::Filters::SelectOptions
10
+ # @!visibility private
11
+ def initialize(grid, name, **options, &block)
12
+ options[:multiple] = true if options[:checkboxes]
13
+ super
14
+ raise Datagrid::ConfigurationError, ":select option not specified" unless options[:select]
15
+ end
6
16
 
7
- def initialize(*args)
8
- super(*args)
9
- if checkboxes?
10
- options[:multiple] = true
11
- end
12
- raise Datagrid::ConfigurationError, ":select option not specified" unless options[:select]
13
- end
17
+ def parse(value)
18
+ return nil if strict && !select.include?(value)
14
19
 
15
- def parse(value)
16
- return nil if self.strict && !select.include?(value)
17
- value
18
- end
20
+ value
21
+ end
19
22
 
20
- def strict
21
- options[:strict]
22
- end
23
+ def default_input_options
24
+ {
25
+ **super,
26
+ type: enum_checkboxes? ? "checkbox" : "select",
27
+ multiple: multiple?,
28
+ include_hidden: enum_checkboxes? ? false : nil,
29
+ }
30
+ end
23
31
 
24
- def checkboxes?
25
- options[:checkboxes]
26
- end
32
+ def label_options
33
+ if enum_checkboxes?
34
+ # Each checkbox has its own label
35
+ # The main label has no specific input to focus
36
+ # See app/views/datagrid/_enum_checkboxes.html.erb
37
+ { for: nil, **super }
38
+ else
39
+ super
40
+ end
41
+ end
42
+
43
+ def strict
44
+ options[:strict]
45
+ end
27
46
 
47
+ def enum_checkboxes?
48
+ options[:checkboxes]
49
+ end
50
+ end
51
+ end
28
52
  end
@@ -1,37 +1,49 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # @!visibility private
2
- class Datagrid::Filters::ExtendedBooleanFilter < Datagrid::Filters::EnumFilter
4
+ module Datagrid
5
+ module Filters
6
+ class ExtendedBooleanFilter < Datagrid::Filters::EnumFilter
7
+ YES = "YES"
8
+ NO = "NO"
9
+ TRUTH_VALUES = [true, "true", "y", "yes"].freeze
10
+ FALSE_VALUES = [false, "false", "n", "no"].freeze
3
11
 
4
- YES = "YES"
5
- NO = "NO"
12
+ def initialize(*args, **options)
13
+ options[:select] = -> { boolean_select }
14
+ super
15
+ end
6
16
 
7
- def initialize(report, attribute, options = {}, &block)
8
- options[:select] = -> { boolean_select }
9
- super(report, attribute, options, &block)
10
- end
17
+ def execute(value, scope, grid_object)
18
+ value = value.blank? ? nil : ::Datagrid::Utils.booleanize(value)
19
+ super
20
+ end
11
21
 
12
- def execute(value, scope, grid_object)
13
- value = value.blank? ? nil : ::Datagrid::Utils.booleanize(value)
14
- super(value, scope, grid_object)
15
- end
22
+ def default_input_options
23
+ { **super, type: "select" }
24
+ end
16
25
 
17
- def parse(value)
18
- case
19
- when value == true
20
- YES
21
- when value == false
22
- NO
23
- when value.blank?
24
- nil
25
- else
26
- super(value)
27
- end
28
- end
26
+ def parse(value)
27
+ value = value.downcase if value.is_a?(String)
28
+ case value
29
+ when *TRUTH_VALUES
30
+ YES
31
+ when *FALSE_VALUES
32
+ NO
33
+ when value.blank?
34
+ nil
35
+ else
36
+ super
37
+ end
38
+ end
29
39
 
30
- protected
40
+ protected
31
41
 
32
- def boolean_select
33
- [YES, NO].map do |key, value|
34
- [I18n.t("datagrid.filters.xboolean.#{key.downcase}"), key]
42
+ def boolean_select
43
+ [YES, NO].map do |key, _value|
44
+ [I18n.t("datagrid.filters.xboolean.#{key.downcase}"), key]
45
+ end
46
+ end
35
47
  end
36
48
  end
37
49
  end
@@ -1,9 +1,20 @@
1
- class Datagrid::Filters::FloatFilter < Datagrid::Filters::BaseFilter
1
+ # frozen_string_literal: true
2
2
 
3
- include Datagrid::Filters::RangedFilter
3
+ # @!visibility private
4
+ module Datagrid
5
+ module Filters
6
+ class FloatFilter < Datagrid::Filters::BaseFilter
7
+ include Datagrid::Filters::RangedFilter
4
8
 
5
- def parse(value)
6
- return nil if value.blank?
7
- value.to_f
9
+ def default_input_options
10
+ { **super, type: "number", step: "any" }
11
+ end
12
+
13
+ def parse(value)
14
+ return nil if value.blank?
15
+
16
+ value.to_f
17
+ end
18
+ end
8
19
  end
9
20
  end
@@ -1,17 +1,28 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "datagrid/filters/ranged_filter"
2
4
 
3
- class Datagrid::Filters::IntegerFilter < Datagrid::Filters::BaseFilter
5
+ module Datagrid
6
+ module Filters
7
+ class IntegerFilter < Datagrid::Filters::BaseFilter
8
+ include Datagrid::Filters::RangedFilter
9
+
10
+ def default_input_options
11
+ { **super, type: "number", step: "1" }
12
+ end
4
13
 
5
- include Datagrid::Filters::RangedFilter
14
+ def parse(value)
15
+ return nil if value.blank?
16
+ if defined?(ActiveRecord) && value.is_a?(ActiveRecord::Base) &&
17
+ value.respond_to?(:id) && value.id.is_a?(Integer)
18
+ return value.id
19
+ end
20
+ return value if value.is_a?(Range)
6
21
 
7
- def parse(value)
8
- return nil if value.blank?
9
- if defined?(ActiveRecord) && value.is_a?(ActiveRecord::Base) &&
10
- value.respond_to?(:id) && value.id.is_a?(Integer)
11
- return value.id
22
+ return nil if value.to_i.zero? && value.is_a?(String) && value !~ %r{\A\s*-?0}
23
+
24
+ value.to_i
25
+ end
12
26
  end
13
- return value if value.is_a?(Range)
14
- value.to_i
15
27
  end
16
28
  end
17
-
@@ -1,56 +1,77 @@
1
- module Datagrid::Filters::RangedFilter
1
+ # frozen_string_literal: true
2
2
 
3
- def initialize(grid, name, options, &block)
4
- super(grid, name, options, &block)
5
- if range?
6
- options[:multiple] = true
7
- end
8
- end
3
+ module Datagrid
4
+ module Filters
5
+ module RangedFilter
6
+ SERIALIZED_RANGE = %r{\A(.*)\.{2,3}(.*)\z}
9
7
 
10
- def parse_values(value)
11
- result = super(value)
12
- return result if !range? || result.nil?
13
- # Simulate single point range
14
- return [result, result] unless result.is_a?(Array)
15
-
16
- case result.size
17
- when 0
18
- nil
19
- when 1
20
- result.first
21
- when 2
22
- if result.first && result.last && result.first > result.last
23
- # If wrong range is given - reverse it to be always valid
24
- result.reverse
25
- elsif !result.first && !result.last
26
- nil
27
- else
28
- result
8
+ def parse_values(value)
9
+ return super unless range?
10
+
11
+ case value
12
+ when String
13
+ if ["..", "..."].include?(value)
14
+ nil
15
+ elsif (match = value.match(SERIALIZED_RANGE))
16
+ to_range(match.captures[0], match.captures[1], value.include?("..."))
17
+ else
18
+ super
19
+ end
20
+ when Hash
21
+ parse_hash(value)
22
+ when Array
23
+ parse_array(value)
24
+ when Range
25
+ to_range(value.begin, value.end)
26
+ else
27
+ result = super
28
+ to_range(result, result)
29
+ end
29
30
  end
30
- else
31
- raise ArgumentError, "Can not create a date range from array of more than two: #{result.inspect}"
32
- end
33
31
 
34
- end
32
+ def range?
33
+ options[:range]
34
+ end
35
35
 
36
- def range?
37
- options[:range]
38
- end
36
+ def default_filter_where(scope, value)
37
+ if range? && value.is_a?(Range)
38
+ scope = driver.greater_equal(scope, name, value.begin) if value.begin
39
+ scope = driver.less_equal(scope, name, value.end) if value.end
40
+ scope
41
+ else
42
+ super
43
+ end
44
+ end
39
45
 
40
- def default_filter_where(scope, value)
41
- if range? && value.is_a?(Array)
42
- left, right = value
43
- if left
44
- scope = driver.greater_equal(scope, name, left)
46
+ protected
47
+
48
+ def parse_hash(result)
49
+ to_range(result[:from] || result["from"], result[:to] || result["to"])
45
50
  end
46
- if right
47
- scope = driver.less_equal(scope, name, right)
51
+
52
+ def to_range(from, to, exclusive = false)
53
+ from = parse(from)
54
+ to = parse(to)
55
+ return nil unless to || from
56
+
57
+ # If wrong range is given - reverse it to be always valid
58
+ from, to = to, from if from && to && from > to
59
+ exclusive ? from...to : from..to
48
60
  end
49
- scope
50
- else
51
- super(scope, value)
52
- end
53
- end
54
61
 
62
+ def parse_array(result)
63
+ first = result.first
64
+ last = result.last
55
65
 
66
+ case result.size
67
+ when 0
68
+ nil
69
+ when 1, 2
70
+ to_range(first, last)
71
+ else
72
+ raise ArgumentError, "Can not create a range from array of more than two elements"
73
+ end
74
+ end
75
+ end
76
+ end
56
77
  end