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 CHANGED
@@ -1,4 +1,4 @@
1
- Copyright 2012 Ryan Graham, Dennis Taylor
1
+ Copyright 2012-2013 Ryan Graham, Dennis Taylor
2
2
  Copyright 2011 Ryan Graham
3
3
 
4
4
  Permission is hereby granted, free of charge, to any person obtaining
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://secure.travis-ci.org/rmg/magic_grid.png?branch=master)](http://travis-ci.org/rmg/magic_grid)
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
- Or a more realistic example:
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
@@ -34,4 +34,4 @@ end
34
34
  desc "Run TestUnit and RSpec tests"
35
35
  task :tests => [:spec, :test]
36
36
 
37
- task :default => :tests
37
+ task :default => :spec
@@ -5,20 +5,19 @@ module MagicGrid
5
5
  class Collection
6
6
 
7
7
  DEFAULTS = {
8
- :per_page => 30,
9
- :searchable => [],
10
- :search_method => :search,
11
- :listener_handler => nil,
12
- :default_col => 0,
13
- :default_order => :asc,
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 => nil,
15
+ :count => nil,
17
16
  }
18
17
 
19
18
  def initialize(collection, opts = {})
20
19
  @collection = collection || []
21
- self.options = opts
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, :searches
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=(opts)
41
- @options = DEFAULTS.merge(opts || {})
41
+ def options
42
+ DEFAULTS.merge(@options || {})
42
43
  end
43
44
 
44
45
  def count_options
45
- @options[:count]
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
- @options.hash.abs.to_s(36)
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__(@options[:search_method], q)
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
- (@options[:search_method] and @collection.respond_to? @options[:search_method])
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 #{@options[:search_method]} well"
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).page(@current_page).per(@per_page)
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
@@ -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]).join ' '
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 = case c
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 => 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 => {},
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 => true,
33
- :search_button => false,
34
- :searcher_size => nil,
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 => I18n.t("magic_grid.no_results").capitalize, # "No results found."
41
- :searcher_label => I18n.t("magic_grid.search.label").capitalize + ': ', # "Search: "
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 => I18n.t("magic_grid.search.button").capitalize # "Search"
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
- if cols_or_opts.is_a? Hash
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
- elsif cols_or_opts.is_a? Array
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, @options
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
- @columns = Column.columns_for_collection(@collection,
68
- @columns,
69
- @options[:searchable])
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
- @current_sort_col = param(:col, @options[:default_col]).to_i
72
- unless (0...@columns.count).cover? @current_sort_col
73
- @current_sort_col = @options[:default_col]
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
- @current_order = order(param(:order, @default_order))
76
- @collection.apply_sort(@columns[@current_sort_col], order_sql(@current_order))
105
+ end
77
106
 
78
- filter_keys = @options[:listeners].values
79
- filters = @params.slice(*filter_keys).reject {|k,v| v.to_s.empty? }
80
- @collection.apply_filter filters
81
- @collection.apply_pagination(current_page)
82
- @collection.apply_search current_search
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
- @collection.per_page = @options[:per_page]
85
- @collection.apply_filter_callback @options[:listener_handler]
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
- @options[:id] || (Column.hash_string(@columns) + @collection.hash_string)
131
+ options[:id] || (Column.hash_string(columns) + magic_collection.hash_string)
96
132
  end
97
133
 
98
134
  def searchable?
99
- @collection.searchable?
135
+ magic_collection.searchable?
100
136
  end
101
137
 
102
138
  def needs_searcher?
103
- @options[:needs_searcher] or (searchable? and not @options[:searcher])
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
- @options[:searcher]
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
- @params.fetch(param_key(key), default)
163
+ params.fetch(param_key(key), default)
120
164
  end
121
165
 
122
166
  def base_params
123
- @params.merge :magic_grid_id => magic_id
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