magic_grid 0.12.4 → 0.12.5
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
-
[![Build Status](https://
|
5
|
+
[![Build Status](https://travis-ci.org/rmg/magic_grid.png?branch=master)](https://travis-ci.org/rmg/magic_grid)
|
5
6
|
[![Code Climate](https://codeclimate.com/badge.png)](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
|