datagrid 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.rspec +1 -0
- data/Gemfile +26 -0
- data/Gemfile.lock +111 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +19 -0
- data/Rakefile +49 -0
- data/Readme.md +88 -0
- data/VERSION +1 -0
- data/datagrid.gemspec +106 -0
- data/lib/datagrid.rb +37 -0
- data/lib/datagrid/columns.rb +105 -0
- data/lib/datagrid/columns/column.rb +46 -0
- data/lib/datagrid/core.rb +99 -0
- data/lib/datagrid/filters.rb +150 -0
- data/lib/datagrid/filters/base_filter.rb +33 -0
- data/lib/datagrid/filters/boolean_enum_filter.rb +21 -0
- data/lib/datagrid/filters/boolean_filter.rb +7 -0
- data/lib/datagrid/filters/composite_filters.rb +41 -0
- data/lib/datagrid/filters/date_filter.rb +10 -0
- data/lib/datagrid/filters/default_filter.rb +5 -0
- data/lib/datagrid/filters/enum_filter.rb +32 -0
- data/lib/datagrid/filters/filter_eval.rb +25 -0
- data/lib/datagrid/filters/integer_filter.rb +7 -0
- data/lib/datagrid/form_builder.rb +56 -0
- data/lib/datagrid/helper.rb +80 -0
- data/lib/datagrid/rspec.rb +68 -0
- data/spec/datagrid/form_builder_spec.rb +68 -0
- data/spec/datagrid/helper_spec.rb +46 -0
- data/spec/datagrid_spec.rb +56 -0
- data/spec/spec_helper.rb +39 -0
- data/spec/support/equal_to_dom.rb +42 -0
- data/spec/support/schema.rb +33 -0
- data/spec/support/simple_report.rb +28 -0
- metadata +278 -0
@@ -0,0 +1,105 @@
|
|
1
|
+
module Datagrid
|
2
|
+
|
3
|
+
class OrderUnsupported < StandardError
|
4
|
+
end
|
5
|
+
|
6
|
+
module Columns
|
7
|
+
require "datagrid/columns/column"
|
8
|
+
|
9
|
+
def self.included(base)
|
10
|
+
base.extend ClassMethods
|
11
|
+
base.class_eval do
|
12
|
+
|
13
|
+
include Datagrid::Core
|
14
|
+
|
15
|
+
datagrid_attribute :order
|
16
|
+
datagrid_attribute :reverse
|
17
|
+
|
18
|
+
|
19
|
+
end
|
20
|
+
base.send :include, InstanceMethods
|
21
|
+
end # self.included
|
22
|
+
|
23
|
+
module ClassMethods
|
24
|
+
|
25
|
+
def columns
|
26
|
+
@columns ||= []
|
27
|
+
end
|
28
|
+
|
29
|
+
def column(name, options = {}, &block)
|
30
|
+
block ||= lambda do
|
31
|
+
self.send(name)
|
32
|
+
end
|
33
|
+
self.columns << Datagrid::Columns::Column.new(self, name, options, &block)
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
end # ClassMethods
|
38
|
+
|
39
|
+
module InstanceMethods
|
40
|
+
|
41
|
+
def header
|
42
|
+
self.class.columns.map(&:header)
|
43
|
+
end
|
44
|
+
|
45
|
+
def row_for(asset)
|
46
|
+
self.class.columns.map do |column|
|
47
|
+
column.value(asset)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def hash_for(asset)
|
52
|
+
result = {}
|
53
|
+
self.class.columns.each do |column|
|
54
|
+
result[column.name] = column.value(asset)
|
55
|
+
end
|
56
|
+
result
|
57
|
+
end
|
58
|
+
|
59
|
+
def data
|
60
|
+
self.assets.map do |asset|
|
61
|
+
self.row_for(asset)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def data_hash
|
66
|
+
self.assets.map do |asset|
|
67
|
+
hash_for(asset)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def assets
|
72
|
+
result = super
|
73
|
+
if self.order
|
74
|
+
column = column_by_name(self.order)
|
75
|
+
raise Datagrid::OrderUnsupported, "Can not sort #{self.inspect} by #{name.inspect}" unless column
|
76
|
+
result = result.order(self.reverse ? column.order : column.desc_order)
|
77
|
+
end
|
78
|
+
result
|
79
|
+
end
|
80
|
+
|
81
|
+
def to_csv(options = {})
|
82
|
+
require "fastercsv"
|
83
|
+
FasterCSV.generate(
|
84
|
+
{:headers => self.header, :write_headers => true}.merge(options)
|
85
|
+
) do |csv|
|
86
|
+
self.data.each do |row|
|
87
|
+
csv << row
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def columns
|
93
|
+
self.class.columns
|
94
|
+
end
|
95
|
+
|
96
|
+
def column_by_name(name)
|
97
|
+
self.columns.find do |col|
|
98
|
+
col.name.to_sym == name.to_sym
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
end # InstanceMethods
|
103
|
+
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
|
2
|
+
class Datagrid::Columns::Column
|
3
|
+
|
4
|
+
attr_accessor :grid, :options, :block, :name
|
5
|
+
|
6
|
+
def initialize(grid, name, options = {}, &block)
|
7
|
+
self.grid = grid
|
8
|
+
self.name = name
|
9
|
+
self.options = options
|
10
|
+
self.block = block
|
11
|
+
end
|
12
|
+
|
13
|
+
def value(object)
|
14
|
+
value_for(object)
|
15
|
+
end
|
16
|
+
|
17
|
+
def value_for(object)
|
18
|
+
object.instance_eval(&self.block)
|
19
|
+
end
|
20
|
+
|
21
|
+
def format
|
22
|
+
self.options[:format]
|
23
|
+
end
|
24
|
+
|
25
|
+
def label
|
26
|
+
self.options[:label]
|
27
|
+
end
|
28
|
+
|
29
|
+
def header
|
30
|
+
self.options[:header] ||
|
31
|
+
I18n.translate(self.name, :scope => "reports.#{self.grid}.columns", :default => self.name.to_s.humanize )
|
32
|
+
end
|
33
|
+
|
34
|
+
def order
|
35
|
+
if options.has_key?(:order)
|
36
|
+
self.options[:order]
|
37
|
+
else
|
38
|
+
grid.scope.column_names.include?(name.to_s) ? [grid.scope.table_name, name].join(".") : nil
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def desc_order
|
43
|
+
order ? order.to_s + " DESC" : nil
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module Datagrid
|
2
|
+
module Core
|
3
|
+
|
4
|
+
def self.included(base)
|
5
|
+
base.extend ClassMethods
|
6
|
+
base.class_eval do
|
7
|
+
|
8
|
+
end
|
9
|
+
base.send :include, InstanceMethods
|
10
|
+
end # self.included
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
|
14
|
+
def datagrid_attribute(name, &block)
|
15
|
+
unless datagrid_attributes.include?(name)
|
16
|
+
block ||= lambda do |value|
|
17
|
+
value
|
18
|
+
end
|
19
|
+
datagrid_attributes << name
|
20
|
+
define_method name do
|
21
|
+
instance_variable_get("@#{name}")
|
22
|
+
end
|
23
|
+
|
24
|
+
define_method :"#{name}=" do |value|
|
25
|
+
instance_variable_set("@#{name}", block.call(value))
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def datagrid_attributes
|
31
|
+
@datagrid_attributes ||= []
|
32
|
+
end
|
33
|
+
|
34
|
+
def scope(&block)
|
35
|
+
if block
|
36
|
+
@scope = block
|
37
|
+
else
|
38
|
+
raise(Datagrid::ConfigurationError, "Scope not defined") unless @scope
|
39
|
+
@scope.call
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def param_name
|
44
|
+
self.to_s.underscore.split('/').last
|
45
|
+
end
|
46
|
+
|
47
|
+
end # ClassMethods
|
48
|
+
|
49
|
+
module InstanceMethods
|
50
|
+
|
51
|
+
def initialize(attributes = nil)
|
52
|
+
super()
|
53
|
+
|
54
|
+
if attributes
|
55
|
+
self.attributes = attributes
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def attributes
|
60
|
+
result = {}
|
61
|
+
self.class.datagrid_attributes.each do |name|
|
62
|
+
result[name] = self[name]
|
63
|
+
end
|
64
|
+
result
|
65
|
+
end
|
66
|
+
|
67
|
+
def [](attribute)
|
68
|
+
self.send(attribute)
|
69
|
+
end
|
70
|
+
|
71
|
+
def []=(attribute, value)
|
72
|
+
self.send(:"#{attribute}=", value)
|
73
|
+
end
|
74
|
+
|
75
|
+
def assets
|
76
|
+
scope.scoped({})
|
77
|
+
end
|
78
|
+
|
79
|
+
def attributes=(attributes)
|
80
|
+
attributes.each do |name, value|
|
81
|
+
self[name] = value
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def paginate(*args, &block)
|
86
|
+
self.assets.paginate(*args, &block)
|
87
|
+
end
|
88
|
+
|
89
|
+
def scope
|
90
|
+
self.class.scope
|
91
|
+
end
|
92
|
+
|
93
|
+
def param_name
|
94
|
+
self.class.param_name
|
95
|
+
end
|
96
|
+
|
97
|
+
end # InstanceMethods
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
module Datagrid
|
2
|
+
module Filters
|
3
|
+
|
4
|
+
require "datagrid/filters/base_filter"
|
5
|
+
require "datagrid/filters/enum_filter"
|
6
|
+
require "datagrid/filters/boolean_enum_filter"
|
7
|
+
require "datagrid/filters/boolean_filter"
|
8
|
+
require "datagrid/filters/date_filter"
|
9
|
+
require "datagrid/filters/default_filter"
|
10
|
+
require "datagrid/filters/filter_eval"
|
11
|
+
require "datagrid/filters/integer_filter"
|
12
|
+
require "datagrid/filters/composite_filters"
|
13
|
+
|
14
|
+
FILTER_TYPES = {
|
15
|
+
:date => Filters::DateFilter,
|
16
|
+
:string => Filters::DefaultFilter,
|
17
|
+
:default => Filters::DefaultFilter,
|
18
|
+
:eboolean => Filters::BooleanEnumFilter ,
|
19
|
+
:boolean => Filters::BooleanFilter ,
|
20
|
+
:integer => Filters::IntegerFilter,
|
21
|
+
:enum => Filters::EnumFilter,
|
22
|
+
}
|
23
|
+
|
24
|
+
def self.included(base)
|
25
|
+
base.extend ClassMethods
|
26
|
+
base.class_eval do
|
27
|
+
|
28
|
+
include Datagrid::Core
|
29
|
+
include Datagrid::Filters::CompositeFilters
|
30
|
+
|
31
|
+
end
|
32
|
+
base.send :include, InstanceMethods
|
33
|
+
end # self.included
|
34
|
+
|
35
|
+
module ClassMethods
|
36
|
+
|
37
|
+
def filters
|
38
|
+
@filters ||= []
|
39
|
+
end
|
40
|
+
|
41
|
+
def filter_by_name(attribute)
|
42
|
+
self.filters.find do |filter|
|
43
|
+
filter.name.to_sym == attribute.to_sym
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
# Defines the accessible attribute that is used to filter
|
49
|
+
# scope by the specified value with specified code.
|
50
|
+
#
|
51
|
+
# Example:
|
52
|
+
#
|
53
|
+
# class UserGrid
|
54
|
+
# include Datagrid
|
55
|
+
#
|
56
|
+
# filter(:name)
|
57
|
+
# filter(:posts_count, :integer) do |value|
|
58
|
+
# self.where(["posts_count >= ?", value])
|
59
|
+
# end
|
60
|
+
#
|
61
|
+
# scope do
|
62
|
+
# User.order("users.created_at desc")
|
63
|
+
# end
|
64
|
+
# end
|
65
|
+
#
|
66
|
+
# Each filter becomes grid atttribute.
|
67
|
+
# In order to create grid that display all users with name 'John' that have more than zero posts:
|
68
|
+
#
|
69
|
+
# grid = UserGrid.new(:posts_count => 1, :name => "John")
|
70
|
+
# grid.assets # SELECT * FROM users WHERE users.posts_count > 1 AND name = 'John'
|
71
|
+
#
|
72
|
+
# Important! Take care about non-breaking the filter chain and force objects loading in filter.
|
73
|
+
# The filter block should always return a <tt>ActiveRecord::Scope</tt> rather than <tt>Array</tt>
|
74
|
+
#
|
75
|
+
# = Default filter block
|
76
|
+
#
|
77
|
+
# If no block given filter is generated automatically as simple select by filter name from scope.
|
78
|
+
#
|
79
|
+
# = Filter types
|
80
|
+
#
|
81
|
+
# Filter does types conversion automatically.
|
82
|
+
# The following filter types are supported:
|
83
|
+
#
|
84
|
+
# * <tt>:string</tt> (default) - converts value to string
|
85
|
+
# * <tt>:date</tt> - converts value to date using date parser
|
86
|
+
# * <tt>:enum</tt> - designed to be collection select. Additional options for easy form generation:
|
87
|
+
# * <tt>:select</tt> (required) - collection of values to match agains.
|
88
|
+
# * <tt>:multiple</tt> - if true multiple values can be assigned to this filter. Default: false.
|
89
|
+
# * <tt>:eboolean</tt> - subtype of enum filter that provides select of "Yes", "No" and "Any". Could be useful.
|
90
|
+
# * <tt>:integer</tt> - converts given value to integer.
|
91
|
+
#
|
92
|
+
#
|
93
|
+
def filter(attribute, type = :string, options = {}, &block)
|
94
|
+
|
95
|
+
klass = type.is_a?(Class) ? type : FILTER_TYPES[type]
|
96
|
+
raise ConfigurationError, "filter class not found" unless klass
|
97
|
+
|
98
|
+
block ||= default_filter(attribute)
|
99
|
+
|
100
|
+
filter = klass.new(self, attribute, options, &block)
|
101
|
+
self.filters << filter
|
102
|
+
|
103
|
+
datagrid_attribute(attribute) do |value|
|
104
|
+
filter.format(value)
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
|
109
|
+
protected
|
110
|
+
def default_filter(attribute)
|
111
|
+
if self.scope.column_names.include?(attribute.to_s)
|
112
|
+
lambda do |value|
|
113
|
+
self.scoped(:conditions => {attribute => value})
|
114
|
+
end
|
115
|
+
else
|
116
|
+
raise ConfigurationError, "Not able to generate default filter. No column '#{attribute}' in #{self.scope.table_name}."
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
end # ClassMethods
|
121
|
+
|
122
|
+
module InstanceMethods
|
123
|
+
|
124
|
+
def initialize(*args, &block)
|
125
|
+
self.filters.each do |filter|
|
126
|
+
self[filter.name] = filter.default
|
127
|
+
end
|
128
|
+
super(*args, &block)
|
129
|
+
end
|
130
|
+
|
131
|
+
def assets
|
132
|
+
result = super
|
133
|
+
self.class.filters.each do |filter|
|
134
|
+
result = filter.apply(result, filter_value(filter))
|
135
|
+
end
|
136
|
+
result
|
137
|
+
end
|
138
|
+
|
139
|
+
def filters
|
140
|
+
self.class.filters
|
141
|
+
end
|
142
|
+
|
143
|
+
def filter_value(filter)
|
144
|
+
self[filter.name]
|
145
|
+
end
|
146
|
+
|
147
|
+
end # InstanceMethods
|
148
|
+
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require "datagrid/filters/filter_eval"
|
2
|
+
|
3
|
+
class Datagrid::Filters::BaseFilter
|
4
|
+
|
5
|
+
attr_accessor :grid, :options, :block, :name
|
6
|
+
|
7
|
+
def initialize(grid, name, options = {}, &block)
|
8
|
+
self.grid = grid
|
9
|
+
self.name = name
|
10
|
+
self.options = options
|
11
|
+
self.block = block
|
12
|
+
end
|
13
|
+
|
14
|
+
def apply(scope, value)
|
15
|
+
return scope if value.nil? && !options[:allow_nil]
|
16
|
+
return scope if value.blank? && !options[:allow_blank]
|
17
|
+
::Datagrid::Filters::FilterEval.new(self, scope, value).run
|
18
|
+
end
|
19
|
+
|
20
|
+
def format(value)
|
21
|
+
raise NotImplementedError, "#format(value) suppose to be overwritten"
|
22
|
+
end
|
23
|
+
|
24
|
+
def header
|
25
|
+
I18n.translate(self.name, :scope => "datagrid.#{grid.class.to_s.underscore.split("/").last}.filters", :default => self.name.to_s.humanize)
|
26
|
+
end
|
27
|
+
|
28
|
+
def default
|
29
|
+
self.options[:default]
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class Datagrid::Filters::BooleanEnumFilter < Datagrid::Filters::EnumFilter
|
2
|
+
|
3
|
+
YES = "YES"
|
4
|
+
NO = "NO"
|
5
|
+
VALUES = {YES => true, NO => false}
|
6
|
+
|
7
|
+
def initialize(report, attribute, options = {}, &block)
|
8
|
+
options[:allow_blank] = true unless options.has_key?(:allow_blank)
|
9
|
+
options[:select] = VALUES.keys
|
10
|
+
super(report, attribute, options, &block)
|
11
|
+
end
|
12
|
+
|
13
|
+
def apply(scope, value)
|
14
|
+
super(scope, to_boolean(value))
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_boolean(value)
|
18
|
+
VALUES[value]
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|