magic_grid 0.12.4 → 0.12.5
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.
- data/MIT-LICENSE +1 -1
- data/README.md +29 -2
- data/Rakefile +1 -1
- data/lib/magic_grid/collection.rb +21 -24
- data/lib/magic_grid/column.rb +7 -2
- data/lib/magic_grid/definition.rb +90 -61
- data/lib/magic_grid/html_grid.rb +118 -147
- data/lib/magic_grid/order.rb +69 -0
- data/lib/magic_grid/version.rb +1 -1
- data/spec/collection_spec.rb +17 -9
- data/spec/definition_spec.rb +1 -15
- data/spec/helpers_spec.rb +5 -0
- data/spec/html_grid_spec.rb +19 -13
- data/spec/order_spec.rb +30 -0
- data/spec/spec_helper.rb +1 -1
- metadata +150 -163
data/MIT-LICENSE
CHANGED
data/README.md
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
MagicGrid
|
|
2
2
|
=========
|
|
3
|
+
© 2011-2013 Ryan Graham, Dennis Taylor
|
|
3
4
|
|
|
4
|
-
[](https://travis-ci.org/rmg/magic_grid)
|
|
5
6
|
[](https://codeclimate.com/github/rmg/magic_grid)
|
|
6
7
|
|
|
7
8
|
Easy collection display grid with column sorting and pagination.
|
|
8
9
|
|
|
10
|
+
[MagicGrid Live Demo](http://magic-grid.herokuapp.com/) (it's not very pretty. [Help?](https://github.com/rmg/magic_grid-demo))
|
|
11
|
+
|
|
9
12
|
Displays a collection (ActiveRelation or Array) wrapped in an html table with server
|
|
10
13
|
side column sorting, filtering hooks, and search bar. Large collections can be
|
|
11
14
|
paginated with either the will_paginate gem or kaminari gem if you use them, or a naive
|
|
@@ -21,11 +24,18 @@ In your `Gemfile`:
|
|
|
21
24
|
|
|
22
25
|
gem 'magic_grid'
|
|
23
26
|
|
|
27
|
+
In your controller:
|
|
28
|
+
|
|
29
|
+
@posts = Post.where(:published => true)
|
|
30
|
+
|
|
24
31
|
In your view:
|
|
25
32
|
|
|
26
33
|
<%= magic_grid(@posts, [:title, :author]) %>
|
|
27
34
|
|
|
28
|
-
|
|
35
|
+
What you'll get is an table with 2 sortable columns. You'll also get pagination if
|
|
36
|
+
you have eitehr Keminari or WillPaginate loaded.
|
|
37
|
+
|
|
38
|
+
You can also do your own row rendering by passing a block:
|
|
29
39
|
|
|
30
40
|
```rhtml
|
|
31
41
|
<%= magic_grid(@posts, [:title, :author, "Actions"]) do |post| %>
|
|
@@ -41,6 +51,19 @@ Or a more realistic example:
|
|
|
41
51
|
<% end %>
|
|
42
52
|
```
|
|
43
53
|
|
|
54
|
+
Advanced Options
|
|
55
|
+
----------------
|
|
56
|
+
|
|
57
|
+
There are a bunch of extra options that can be passed to the `magic_grid` helper:
|
|
58
|
+
|
|
59
|
+
### :searchable
|
|
60
|
+
An array of columns to try to generate a search query for. Providing this
|
|
61
|
+
list tells magic_grid to render a search box in the header of the html table
|
|
62
|
+
it generates. Make sure to include `magic_grid.js` in your view or application wide via your application.js or search won't work.
|
|
63
|
+
|
|
64
|
+
### :per_page
|
|
65
|
+
Sets the number of rows per page in the paginator.
|
|
66
|
+
|
|
44
67
|
Development
|
|
45
68
|
-----------
|
|
46
69
|
|
|
@@ -49,3 +72,7 @@ bunch of random page renderings. I've since added some RSpec goodness.
|
|
|
49
72
|
|
|
50
73
|
To run all the tests, just run `rake`.
|
|
51
74
|
|
|
75
|
+
License
|
|
76
|
+
-------
|
|
77
|
+
|
|
78
|
+
Distributed under the MIT license. See [MIT-LICENSE](MIT-LICENSE) for detail.
|
data/Rakefile
CHANGED
|
@@ -5,20 +5,19 @@ module MagicGrid
|
|
|
5
5
|
class Collection
|
|
6
6
|
|
|
7
7
|
DEFAULTS = {
|
|
8
|
-
:per_page
|
|
9
|
-
:searchable
|
|
10
|
-
:search_method
|
|
11
|
-
:listener_handler
|
|
12
|
-
:default_col
|
|
13
|
-
:
|
|
14
|
-
:post_filter => false,
|
|
8
|
+
:per_page => 30,
|
|
9
|
+
:searchable => [],
|
|
10
|
+
:search_method => :search,
|
|
11
|
+
:listener_handler => nil,
|
|
12
|
+
:default_col => 0,
|
|
13
|
+
:post_filter => false,
|
|
15
14
|
:collection_post_filter => true,
|
|
16
|
-
:count
|
|
15
|
+
:count => nil,
|
|
17
16
|
}
|
|
18
17
|
|
|
19
18
|
def initialize(collection, opts = {})
|
|
20
19
|
@collection = collection || []
|
|
21
|
-
|
|
20
|
+
@options = opts
|
|
22
21
|
@current_page = 1
|
|
23
22
|
@sorts = []
|
|
24
23
|
@filter_callbacks = []
|
|
@@ -34,15 +33,17 @@ module MagicGrid
|
|
|
34
33
|
delegate :quoted_table_name, :map, :count, :to => :collection
|
|
35
34
|
|
|
36
35
|
attr_accessor :searchable_columns
|
|
37
|
-
attr_reader :current_page, :original_count, :total_pages, :per_page,
|
|
36
|
+
attr_reader :current_page, :original_count, :total_pages, :per_page,
|
|
37
|
+
:searches
|
|
38
|
+
attr_writer :options
|
|
38
39
|
cattr_accessor :kaminari_class
|
|
39
40
|
|
|
40
|
-
def options
|
|
41
|
-
|
|
41
|
+
def options
|
|
42
|
+
DEFAULTS.merge(@options || {})
|
|
42
43
|
end
|
|
43
44
|
|
|
44
45
|
def count_options
|
|
45
|
-
|
|
46
|
+
options[:count]
|
|
46
47
|
end
|
|
47
48
|
|
|
48
49
|
def self.create_or_reuse(collection, opts = {})
|
|
@@ -73,12 +74,12 @@ module MagicGrid
|
|
|
73
74
|
if @collection.respond_to? :to_sql
|
|
74
75
|
@collection.to_sql.hash.abs.to_s(36)
|
|
75
76
|
else
|
|
76
|
-
|
|
77
|
+
options.hash.abs.to_s(36)
|
|
77
78
|
end
|
|
78
79
|
end
|
|
79
80
|
|
|
80
81
|
def search_using_builtin(collection, q)
|
|
81
|
-
collection.__send__(
|
|
82
|
+
collection.__send__(options[:search_method], q)
|
|
82
83
|
end
|
|
83
84
|
|
|
84
85
|
def search_using_where(collection, q)
|
|
@@ -104,12 +105,12 @@ module MagicGrid
|
|
|
104
105
|
@reduced_collection = nil
|
|
105
106
|
@sorts << "#{col.custom_sql} #{dir}"
|
|
106
107
|
end
|
|
107
|
-
self
|
|
108
108
|
end
|
|
109
109
|
|
|
110
110
|
def searchable?
|
|
111
111
|
(filterable? and not searchable_columns.empty?) or
|
|
112
|
-
(
|
|
112
|
+
(options[:search_method] and
|
|
113
|
+
@collection.respond_to? options[:search_method])
|
|
113
114
|
end
|
|
114
115
|
|
|
115
116
|
def apply_search(q)
|
|
@@ -121,13 +122,12 @@ module MagicGrid
|
|
|
121
122
|
MagicGrid.logger.warn "#{self.class.name}: Ignoring searchable fields on collection"
|
|
122
123
|
end
|
|
123
124
|
end
|
|
124
|
-
self
|
|
125
125
|
end
|
|
126
126
|
|
|
127
127
|
def perform_search(collection, q)
|
|
128
128
|
search_using_builtin(collection, q)
|
|
129
129
|
rescue
|
|
130
|
-
MagicGrid.logger.debug "Given collection doesn't respond to #{
|
|
130
|
+
MagicGrid.logger.debug "Given collection doesn't respond to #{options[:search_method]} well"
|
|
131
131
|
search_using_where(collection, q)
|
|
132
132
|
end
|
|
133
133
|
|
|
@@ -140,7 +140,6 @@ module MagicGrid
|
|
|
140
140
|
@reduced_collection = nil
|
|
141
141
|
@filters << filters
|
|
142
142
|
end
|
|
143
|
-
self
|
|
144
143
|
end
|
|
145
144
|
|
|
146
145
|
def apply_filter_callback(callback)
|
|
@@ -148,7 +147,6 @@ module MagicGrid
|
|
|
148
147
|
@reduced_collection = nil
|
|
149
148
|
@filter_callbacks << callback
|
|
150
149
|
end
|
|
151
|
-
self
|
|
152
150
|
end
|
|
153
151
|
|
|
154
152
|
def add_post_filter_callback(callback)
|
|
@@ -156,7 +154,6 @@ module MagicGrid
|
|
|
156
154
|
@reduced_collection = nil
|
|
157
155
|
@post_filter_callbacks << callback
|
|
158
156
|
end
|
|
159
|
-
self
|
|
160
157
|
end
|
|
161
158
|
|
|
162
159
|
def has_post_filter?
|
|
@@ -192,7 +189,6 @@ module MagicGrid
|
|
|
192
189
|
def apply_pagination(current_page)
|
|
193
190
|
@current_page = current_page
|
|
194
191
|
@reduced_collection = nil
|
|
195
|
-
self
|
|
196
192
|
end
|
|
197
193
|
|
|
198
194
|
def default_paginate(collection, page, per_page)
|
|
@@ -221,7 +217,8 @@ module MagicGrid
|
|
|
221
217
|
elsif collection.respond_to? :page
|
|
222
218
|
collection.page(@current_page).per(@per_page)
|
|
223
219
|
elsif collection.is_a?(Array) and @@kaminari_class
|
|
224
|
-
@@kaminari_class.paginate_array(collection).
|
|
220
|
+
@@kaminari_class.paginate_array(collection).
|
|
221
|
+
page(@current_page).per(@per_page)
|
|
225
222
|
else
|
|
226
223
|
default_paginate(collection, @current_page, @per_page)
|
|
227
224
|
end
|
data/lib/magic_grid/column.rb
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
require 'magic_grid/order'
|
|
2
|
+
|
|
1
3
|
module MagicGrid
|
|
2
4
|
class Column
|
|
5
|
+
attr_accessor :order
|
|
3
6
|
|
|
4
7
|
def self.columns_for_collection(collection, columns, searchables)
|
|
5
8
|
columns.each_with_index.map { |c, i|
|
|
@@ -56,7 +59,8 @@ module MagicGrid
|
|
|
56
59
|
end
|
|
57
60
|
|
|
58
61
|
def html_classes
|
|
59
|
-
Array(@col[:class])
|
|
62
|
+
@html_classes ||= (Array(@col[:class]) << order.css_class)
|
|
63
|
+
@html_classes.join(' ')
|
|
60
64
|
end
|
|
61
65
|
|
|
62
66
|
def reader
|
|
@@ -66,7 +70,7 @@ module MagicGrid
|
|
|
66
70
|
private
|
|
67
71
|
def initialize(collection, c, i)
|
|
68
72
|
@collection = collection
|
|
69
|
-
@col =
|
|
73
|
+
@col = case c
|
|
70
74
|
when Symbol
|
|
71
75
|
{:col => c}
|
|
72
76
|
when String
|
|
@@ -86,6 +90,7 @@ module MagicGrid
|
|
|
86
90
|
class FilterOnlyColumn < Column
|
|
87
91
|
attr_reader :name, :custom_sql
|
|
88
92
|
def initialize(name, collection = nil)
|
|
93
|
+
@order = Order::Unordered
|
|
89
94
|
@name = name
|
|
90
95
|
if collection
|
|
91
96
|
@custom_sql = collection.quote_column_name(name)
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
require 'magic_grid/logger'
|
|
2
2
|
require 'magic_grid/collection'
|
|
3
3
|
require 'magic_grid/column'
|
|
4
|
+
require 'magic_grid/order'
|
|
4
5
|
require 'active_support/core_ext'
|
|
5
6
|
|
|
6
7
|
module MagicGrid
|
|
7
8
|
class Definition
|
|
8
|
-
attr_reader :columns, :options, :params
|
|
9
|
-
:current_sort_col, :current_order, :default_order
|
|
9
|
+
attr_reader :columns, :options, :params
|
|
10
10
|
|
|
11
11
|
def magic_collection
|
|
12
12
|
@collection
|
|
@@ -17,38 +17,41 @@ module MagicGrid
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
DEFAULTS = {
|
|
20
|
-
:class
|
|
21
|
-
:top_pager
|
|
22
|
-
:bottom_pager
|
|
23
|
-
:remote
|
|
24
|
-
:min_search_length
|
|
25
|
-
:id
|
|
26
|
-
:searcher
|
|
27
|
-
:needs_searcher
|
|
28
|
-
:live_search
|
|
29
|
-
:listeners
|
|
20
|
+
:class => [],
|
|
21
|
+
:top_pager => false,
|
|
22
|
+
:bottom_pager => true,
|
|
23
|
+
:remote => false,
|
|
24
|
+
:min_search_length => 3,
|
|
25
|
+
:id => false,
|
|
26
|
+
:searcher => false,
|
|
27
|
+
:needs_searcher => false,
|
|
28
|
+
:live_search => false,
|
|
29
|
+
:listeners => {},
|
|
30
30
|
:collapse_empty_header => false,
|
|
31
31
|
:collapse_empty_footer => false,
|
|
32
|
-
:default_ajax_handler
|
|
33
|
-
:
|
|
34
|
-
:
|
|
32
|
+
:default_ajax_handler => true,
|
|
33
|
+
:default_order => :asc,
|
|
34
|
+
:search_button => false,
|
|
35
|
+
:searcher_size => nil,
|
|
36
|
+
:title => nil,
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
def self.runtime_defaults
|
|
38
40
|
# run these lazily to catch any late I18n path changes
|
|
39
41
|
DEFAULTS.merge(Collection::DEFAULTS).merge(
|
|
40
|
-
:if_empty
|
|
41
|
-
:searcher_label
|
|
42
|
+
:if_empty => I18n.t("magic_grid.no_results").capitalize, # "No results found."
|
|
43
|
+
:searcher_label => I18n.t("magic_grid.search.label").capitalize + ': ', # "Search: "
|
|
42
44
|
:searcher_tooltip => I18n.t("magic_grid.search.tooltip"), # "type.. + <return>"
|
|
43
|
-
:searcher_button
|
|
45
|
+
:searcher_button => I18n.t("magic_grid.search.button").capitalize # "Search"
|
|
44
46
|
)
|
|
45
47
|
end
|
|
46
48
|
|
|
47
49
|
def self.normalize_columns_options(cols_or_opts, opts)
|
|
48
|
-
|
|
50
|
+
case cols_or_opts
|
|
51
|
+
when Hash
|
|
49
52
|
options = runtime_defaults.merge(cols_or_opts.reject {|k| k == :cols})
|
|
50
53
|
columns = cols_or_opts.fetch(:cols, [])
|
|
51
|
-
|
|
54
|
+
when Array
|
|
52
55
|
options = runtime_defaults.merge opts
|
|
53
56
|
columns = cols_or_opts
|
|
54
57
|
else
|
|
@@ -59,87 +62,113 @@ module MagicGrid
|
|
|
59
62
|
|
|
60
63
|
def initialize(cols_or_opts, collection = nil, controller = nil, opts = {})
|
|
61
64
|
@options, @columns = *self.class.normalize_columns_options(cols_or_opts, opts)
|
|
62
|
-
@default_order = @options[:default_order]
|
|
63
65
|
@params = controller && controller.params || {}
|
|
64
66
|
|
|
65
|
-
@collection = Collection.create_or_reuse collection,
|
|
67
|
+
@collection = Collection.create_or_reuse collection, options
|
|
68
|
+
|
|
69
|
+
@columns = Column.columns_for_collection(magic_collection,
|
|
70
|
+
columns,
|
|
71
|
+
options[:searchable])
|
|
72
|
+
columns.each do |col|
|
|
73
|
+
if col.sortable?
|
|
74
|
+
if col.id == current_sort_col
|
|
75
|
+
col.order = current_order
|
|
76
|
+
else
|
|
77
|
+
col.order = Order::Unordered
|
|
78
|
+
end
|
|
79
|
+
else
|
|
80
|
+
col.order = Order::Unsortable
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
apply_collection_params
|
|
85
|
+
end
|
|
66
86
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
87
|
+
def apply_collection_params
|
|
88
|
+
magic_collection.apply_sort(columns[current_sort_col], current_order.to_sql)
|
|
89
|
+
|
|
90
|
+
magic_collection.apply_filter filters
|
|
91
|
+
magic_collection.apply_pagination(current_page)
|
|
92
|
+
magic_collection.apply_search current_search
|
|
93
|
+
|
|
94
|
+
magic_collection.per_page = options[:per_page]
|
|
95
|
+
magic_collection.apply_filter_callback options[:listener_handler]
|
|
96
|
+
magic_collection.enable_post_filter options[:collection_post_filter]
|
|
97
|
+
magic_collection.add_post_filter_callback options[:post_filter]
|
|
98
|
+
end
|
|
70
99
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
100
|
+
def filters
|
|
101
|
+
@filters ||= begin
|
|
102
|
+
filter_keys = options[:listeners].values
|
|
103
|
+
params.slice(*filter_keys).reject {|k,v| v.to_s.empty? }
|
|
74
104
|
end
|
|
75
|
-
|
|
76
|
-
@collection.apply_sort(@columns[@current_sort_col], order_sql(@current_order))
|
|
105
|
+
end
|
|
77
106
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
107
|
+
def current_sort_col
|
|
108
|
+
@current_sort_col ||= begin
|
|
109
|
+
given = param(:col, -1)
|
|
110
|
+
if given >= 0 and given <= columns.count
|
|
111
|
+
given
|
|
112
|
+
else
|
|
113
|
+
options[:default_col].to_i
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def default_order
|
|
119
|
+
@default_order ||= Order.from_param(options[:default_order])
|
|
120
|
+
end
|
|
83
121
|
|
|
84
|
-
|
|
85
|
-
@
|
|
86
|
-
@collection.enable_post_filter @options[:collection_post_filter]
|
|
87
|
-
@collection.add_post_filter_callback @options[:post_filter]
|
|
122
|
+
def current_order
|
|
123
|
+
@current_order ||= Order.from_param(param(:order, default_order.to_param))
|
|
88
124
|
end
|
|
89
125
|
|
|
90
126
|
def current_search
|
|
91
|
-
param(:q)
|
|
127
|
+
param(:q, "")
|
|
92
128
|
end
|
|
93
129
|
|
|
94
130
|
def magic_id
|
|
95
|
-
|
|
131
|
+
options[:id] || (Column.hash_string(columns) + magic_collection.hash_string)
|
|
96
132
|
end
|
|
97
133
|
|
|
98
134
|
def searchable?
|
|
99
|
-
|
|
135
|
+
magic_collection.searchable?
|
|
100
136
|
end
|
|
101
137
|
|
|
102
138
|
def needs_searcher?
|
|
103
|
-
|
|
139
|
+
options[:needs_searcher] or (searchable? and not options[:searcher])
|
|
104
140
|
end
|
|
105
141
|
|
|
106
142
|
def searcher
|
|
107
143
|
if needs_searcher?
|
|
108
144
|
param_key(:searcher)
|
|
109
145
|
else
|
|
110
|
-
|
|
146
|
+
options[:searcher]
|
|
111
147
|
end
|
|
112
148
|
end
|
|
113
149
|
|
|
150
|
+
def has_title?
|
|
151
|
+
options[:title] || false
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def title
|
|
155
|
+
options[:title]
|
|
156
|
+
end
|
|
157
|
+
|
|
114
158
|
def param_key(key)
|
|
115
159
|
"#{magic_id}_#{key}".to_sym
|
|
116
160
|
end
|
|
117
161
|
|
|
118
162
|
def param(key, default=nil)
|
|
119
|
-
|
|
163
|
+
params.fetch(param_key(key), default)
|
|
120
164
|
end
|
|
121
165
|
|
|
122
166
|
def base_params
|
|
123
|
-
|
|
167
|
+
params.merge :magic_grid_id => magic_id
|
|
124
168
|
end
|
|
125
169
|
|
|
126
170
|
def current_page
|
|
127
171
|
[param(:page, 1).to_i, 1].max
|
|
128
172
|
end
|
|
129
|
-
|
|
130
|
-
def order(something)
|
|
131
|
-
case something
|
|
132
|
-
when 1, "1", :desc, :DESC, "desc", "DESC"
|
|
133
|
-
1
|
|
134
|
-
#when 0, "0", :asc, :ASC, "asc", "ASC"
|
|
135
|
-
# 0
|
|
136
|
-
else
|
|
137
|
-
0
|
|
138
|
-
end
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
def order_sql(something)
|
|
142
|
-
["ASC", "DESC"][order(something)]
|
|
143
|
-
end
|
|
144
173
|
end
|
|
145
174
|
end
|