magic_grid 0.11.1 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -2,6 +2,7 @@ MagicGrid
2
2
  =========
3
3
 
4
4
  [![Build Status](https://secure.travis-ci.org/rmg/magic_grid.png?branch=master)](http://travis-ci.org/rmg/magic_grid)
5
+ [![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/rmg/magic_grid)
5
6
 
6
7
  Easy collection display grid with column sorting and pagination.
7
8
 
data/Rakefile CHANGED
@@ -32,6 +32,6 @@ RSpec::Core::RakeTask.new(:spec) do |t|
32
32
  end
33
33
 
34
34
  desc "Run TestUnit and RSpec tests"
35
- task :tests => [:spec, :test]
35
+ task tests: [:spec, :test]
36
36
 
37
- task :default => :tests
37
+ task default: :tests
@@ -127,8 +127,11 @@ $(function () {
127
127
  var $grid = $(this),
128
128
  listeners = $grid.data("listeners"),
129
129
  grid_id = this.id,
130
- handler = function (change) {
131
- if (! $("#"+change.data.field).hasClass("ready")) {
130
+ mark_as_ready = function () {
131
+ $(this).addClass("ready");
132
+ },
133
+ request_grid_if_ready = function () {
134
+ if (! $(this).hasClass("ready")) {
132
135
  return;
133
136
  }
134
137
  var base_url = $grid.data("current").split("?", 2)[0];
@@ -143,10 +146,8 @@ $(function () {
143
146
  }
144
147
  };
145
148
  for (var k in listeners) {
146
- $("#" + k).on("change", {"field": listeners[k]}, handler);
147
- $("#" + k).on("ready", function (ready) {
148
- $(this).addClass("ready");
149
- });
149
+ $("#" + k).on("change", request_grid_if_ready);
150
+ $("#" + k).on("ready", mark_as_ready);
150
151
  }
151
152
  });
152
153
 
@@ -4,9 +4,20 @@ require 'magic_grid/logger'
4
4
  module MagicGrid
5
5
  class Collection
6
6
 
7
- def initialize(collection, grid)
8
- @collection = collection
9
- @grid = grid
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,
15
+ collection_post_filter: true,
16
+ }
17
+
18
+ def initialize(collection, opts = {})
19
+ @collection = collection || []
20
+ self.options = opts
10
21
  @current_page = 1
11
22
  @sorts = []
12
23
  @filter_callbacks = []
@@ -15,58 +26,61 @@ module MagicGrid
15
26
  @post_filters = []
16
27
  @post_filter_callbacks = []
17
28
  @paginations = []
29
+ @searchable_columns = []
18
30
  end
19
31
 
20
- delegate :quoted_table_name, :map, :count, :to => :collection
32
+ delegate :quoted_table_name, :map, :count, to: :collection
21
33
 
22
- attr_accessor :grid
23
- attr_reader :current_page, :original_count, :total_pages
34
+ attr_accessor :searchable_columns
35
+ attr_reader :current_page, :original_count, :total_pages, :per_page, :searches
24
36
 
25
- def self.[](collection, grid)
37
+ def options=(opts)
38
+ @options = DEFAULTS.merge(opts || {})
39
+ end
40
+
41
+ def self.create_or_reuse(collection, opts = {})
26
42
  if collection.is_a?(self)
27
- collection.grid = grid
43
+ collection.options = opts
28
44
  collection
29
45
  else
30
- Collection.new(collection, grid)
46
+ Collection.new(collection, opts)
31
47
  end
32
48
  end
33
49
 
34
50
  def column_names
35
- @collection.table.columns.map {|c| c.name}
51
+ @collection.table.columns.map{|c| c[:name]}
52
+ rescue
53
+ MagicGrid.logger.debug("Given collection doesn't respond to #table well: #{$!}")
54
+ []
36
55
  end
37
56
 
38
57
  def quote_column_name(col)
39
- @collection.connection.quote_column_name(col.to_s)
58
+ if col.is_a? Symbol and @collection.respond_to? :quoted_table_name
59
+ "#{quoted_table_name}.#{@collection.connection.quote_column_name(col.to_s)}"
60
+ else
61
+ col.to_s
62
+ end
63
+ end
64
+
65
+ def hash_string
66
+ if @collection.respond_to? :to_sql
67
+ @collection.to_sql.hash.abs.to_s(36)
68
+ else
69
+ @options.hash.abs.to_s(36)
70
+ end
40
71
  end
41
72
 
42
73
  def search_using_builtin(collection, q)
43
- collection.__send__(@grid.options[:search_method], q)
74
+ collection.__send__(@options[:search_method], q)
44
75
  end
45
76
 
46
77
  def search_using_where(collection, q)
47
78
  result = collection
48
- search_cols = @grid.options[:searchable].map do |searchable|
49
- case searchable
50
- when Symbol
51
- known = @grid.columns.find {|col| col[:col] == searchable}
52
- if known and known.key?(:sql)
53
- known[:sql]
54
- else
55
- "#{@collection.table_name}.#{quote_column_name(searchable)}"
56
- end
57
- when Integer
58
- @grid.columns[searchable][:sql]
59
- when String
60
- searchable
61
- else
62
- raise "Searchable must be identifiable"
63
- end
64
- end
65
-
66
- unless search_cols.empty?
79
+ unless searchable_columns.empty?
67
80
  begin
81
+ search_cols = searchable_columns.map {|c| c.custom_sql || c.name }
68
82
  clauses = search_cols.map {|c| c << " LIKE :search" }.join(" OR ")
69
- result = collection.where(clauses, {:search => "%#{q}%"})
83
+ result = collection.where(clauses, {search: "%#{q}%"})
70
84
  rescue
71
85
  MagicGrid.logger.debug "Given collection doesn't respond to :where well"
72
86
  end
@@ -79,25 +93,34 @@ module MagicGrid
79
93
  end
80
94
 
81
95
  def apply_sort(col, dir)
82
- @reduced_collection = nil
83
- @sorts << "#{col} #{dir}"
96
+ if sortable? and col.sortable?
97
+ @reduced_collection = nil
98
+ @sorts << "#{col.custom_sql} #{dir}"
99
+ end
84
100
  self
85
101
  end
86
102
 
87
103
  def searchable?
88
- filterable? or @collection.respond_to? @grid.options[:search_method]
104
+ (filterable? and not searchable_columns.empty?) or
105
+ (@options[:search_method] and @collection.respond_to? @options[:search_method])
89
106
  end
90
107
 
91
108
  def apply_search(q)
92
- @reduced_collection = nil
93
- @searches << q
109
+ if q and not q.empty?
110
+ if searchable?
111
+ @reduced_collection = nil
112
+ @searches << q
113
+ else
114
+ MagicGrid.logger.warn "#{self.class.name}: Ignoring searchable fields on collection"
115
+ end
116
+ end
94
117
  self
95
118
  end
96
119
 
97
120
  def perform_search(collection, q)
98
121
  search_using_builtin(collection, q)
99
122
  rescue
100
- MagicGrid.logger.debug "Given collection doesn't respond to #{@grid.options[:search_method]} well"
123
+ MagicGrid.logger.debug "Given collection doesn't respond to #{@options[:search_method]} well"
101
124
  search_using_where(collection, q)
102
125
  end
103
126
 
@@ -106,7 +129,7 @@ module MagicGrid
106
129
  end
107
130
 
108
131
  def apply_filter(filters = {})
109
- if @collection.respond_to? :where
132
+ if filterable? and not filters.empty?
110
133
  @reduced_collection = nil
111
134
  @filters << filters
112
135
  end
@@ -121,45 +144,73 @@ module MagicGrid
121
144
  self
122
145
  end
123
146
 
147
+ def add_post_filter_callback(callback)
148
+ if callback.respond_to? :call
149
+ @reduced_collection = nil
150
+ @post_filter_callbacks << callback
151
+ end
152
+ self
153
+ end
154
+
124
155
  def has_post_filter?
125
156
  @collection.respond_to? :post_filter
126
157
  end
127
158
 
128
- def apply_post_filter
159
+ def enable_post_filter(yes = true)
129
160
  @reduced_collection = nil
130
- @post_filters << :post_filter
161
+ if yes and has_post_filter?
162
+ @post_filters << :post_filter
163
+ end
131
164
  self
132
165
  end
133
166
 
134
- def apply_pagination(current_page, per_page)
135
- if per_page
136
- @reduced_collection = nil
137
- @paginations << {:current_page => current_page, :per_page => per_page}
167
+ def count(collection = nil)
168
+ count_or_hash = collection || @collection
169
+ while count_or_hash.respond_to? :count
170
+ count_or_hash = count_or_hash.count
171
+ end
172
+ count_or_hash
173
+ end
174
+
175
+ def per_page=(n)
176
+ @original_count = self.count @collection
177
+ @per_page = n
178
+ if @per_page
179
+ @total_pages = @original_count / @per_page
180
+ else
181
+ @total_pages = 1
138
182
  end
139
- self
140
183
  end
141
184
 
142
- def perform_pagination(collection, current_page, per_page)
143
- @original_count = @collection.count
144
- @total_pages = @original_count / per_page
185
+ def apply_pagination(current_page)
145
186
  @current_page = current_page
187
+ @reduced_collection = nil
188
+ self
189
+ end
190
+
191
+ def default_paginate(collection, page, per_page)
192
+ collection = collection.to_enum
193
+ collection = collection.each_slice(@per_page)
194
+ collection = collection.drop(@current_page - 1)
195
+ collection = collection.first.to_a
196
+ class << collection
197
+ attr_accessor :current_page, :total_pages, :original_count
198
+ end
199
+ collection
200
+ end
201
+
202
+ def perform_pagination(collection)
203
+ return collection unless @per_page
204
+
146
205
  if collection.respond_to? :paginate
147
- collection = collection.paginate(:page => current_page,
148
- :per_page => per_page)
206
+ collection.paginate(page: @current_page, per_page: @per_page)
149
207
  elsif collection.respond_to? :page
150
- collection = collection.page(current_page).per(per_page)
208
+ collection.page(@current_page).per(@per_page)
151
209
  elsif collection.is_a?(Array) and Module.const_defined?(:Kaminari)
152
- collection = Kaminari.paginate_array(collection).page(current_page).per(per_page)
210
+ Kaminari.paginate_array(collection).page(@current_page).per(@per_page)
153
211
  else
154
- collection = collection.to_enum
155
- collection = collection.each_slice(per_page)
156
- collection = collection.drop(current_page - 1)
157
- collection = collection.first.to_a
158
- class << collection
159
- attr_accessor :current_page, :total_pages, :original_count
160
- end
212
+ default_paginate(collection, @current_page, @per_page)
161
213
  end
162
- collection
163
214
  end
164
215
 
165
216
  def apply_all_operations(collection)
@@ -175,23 +226,20 @@ module MagicGrid
175
226
  @searches.each do |query|
176
227
  collection = perform_search(collection, query)
177
228
  end
229
+ # Do collection filter first, may convert from AR to Array
178
230
  @post_filters.each do |filter|
179
231
  collection = collection.__send__(filter)
180
232
  end
181
233
  @post_filter_callbacks.each do |callback|
182
234
  collection = callback.call(collection)
183
235
  end
184
- @paginations.each do |params|
185
- collection = perform_pagination(collection, params[:current_page], params[:per_page])
186
- end
187
- collection
236
+ # Paginate at the very end, after all sorting, filtering, etc..
237
+ perform_pagination(collection)
188
238
  end
189
239
 
190
240
  def collection
191
241
  @reduced_collection ||= apply_all_operations(@collection)
192
242
  end
193
243
 
194
-
195
-
196
244
  end
197
245
  end
@@ -0,0 +1,93 @@
1
+ module MagicGrid
2
+ class Column
3
+
4
+ def self.columns_for_collection(collection, columns, searchables)
5
+ columns.map.each_with_index { |c, i|
6
+ MagicGrid::Column.new(collection, c, i)
7
+ }.tap do |cols|
8
+ search_disabled = false
9
+ collection.searchable_columns = Array(searchables).map { |searchable|
10
+ case searchable
11
+ when Symbol
12
+ cols.find {|col| col.name == searchable} || FilterOnlyColumn.new(searchable, collection)
13
+ when Integer
14
+ cols[searchable]
15
+ when String
16
+ FilterOnlyColumn.new(searchable)
17
+ when true
18
+ nil
19
+ when false
20
+ search_disabled = true
21
+ nil
22
+ else
23
+ raise "Searchable must be identifiable: #{searchable}"
24
+ end
25
+ }.compact
26
+ collection.searchable_columns = [] if search_disabled
27
+ end
28
+ end
29
+
30
+ def self.hash_string(column_or_columns)
31
+ Array(column_or_columns).map(&:label).join.hash.abs.to_s(36)
32
+ end
33
+
34
+ def label
35
+ @col[:label]
36
+ end
37
+
38
+ def sortable?
39
+ not custom_sql.blank?
40
+ end
41
+
42
+ def custom_sql
43
+ @col[:sql]
44
+ end
45
+
46
+ def id
47
+ @col[:id]
48
+ end
49
+
50
+ def name
51
+ @col[:col]
52
+ end
53
+
54
+ def html_classes
55
+ Array(@col[:class]).join ' '
56
+ end
57
+
58
+ def reader
59
+ @col[:to_s] || @col[:col]
60
+ end
61
+
62
+ private
63
+ def initialize(collection, c, i)
64
+ @collection = collection
65
+ @col = case c
66
+ when Symbol
67
+ {col: c}
68
+ when String
69
+ {label: c}
70
+ else
71
+ c
72
+ end
73
+ @col[:id] = i
74
+ if @collection.column_names.include?(@col[:col])
75
+ @col[:sql] ||= @collection.quote_column_name(name)
76
+ end
77
+ @col[:label] ||= @col[:col].to_s.titleize
78
+ end
79
+
80
+ end
81
+
82
+ class FilterOnlyColumn < Column
83
+ attr_reader :name, :custom_sql
84
+ def initialize(name, collection = nil)
85
+ @name = name
86
+ if collection
87
+ @custom_sql = collection.quote_column_name(name)
88
+ else
89
+ @custom_sql = name
90
+ end
91
+ end
92
+ end
93
+ end
@@ -1,10 +1,12 @@
1
1
  require 'magic_grid/logger'
2
2
  require 'magic_grid/collection'
3
+ require 'magic_grid/column'
4
+ require 'active_support/core_ext'
3
5
 
4
6
  module MagicGrid
5
7
  class Definition
6
- attr_reader :columns, :magic_id, :options, :params,
7
- :current_sort_col, :current_order, :default_order, :per_page
8
+ attr_reader :columns, :options, :params,
9
+ :current_sort_col, :current_order, :default_order
8
10
 
9
11
  def magic_collection
10
12
  @collection
@@ -15,142 +17,87 @@ module MagicGrid
15
17
  end
16
18
 
17
19
  DEFAULTS = {
18
- :class => [],
19
- :top_pager => false,
20
- :bottom_pager => true,
21
- :remote => false,
22
- :per_page => 30,
23
- :searchable => [],
24
- :search_method => :search,
25
- :min_search_length => 3,
26
- :id => false,
27
- :searcher => false,
28
- :needs_searcher => false,
29
- :live_search => false,
30
- :current_search => nil,
31
- :listeners => {},
32
- :listener_handler => nil,
33
- :default_col => 0,
34
- :default_order => :asc,
35
- :empty_header => false,
36
- :empty_footer => false,
37
- :post_filter => false,
38
- :collection_post_filter? => true,
39
- :default_ajax_handler => true,
40
- :search_button => false,
41
- :searcher_size => nil,
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
+ collapse_empty_header: false,
31
+ collapse_empty_footer: false,
32
+ default_ajax_handler: true,
33
+ search_button: false,
34
+ searcher_size: nil,
42
35
  }
43
36
 
44
37
  def self.runtime_defaults
45
38
  # run these lazily to catch any late I18n path changes
46
- DEFAULTS.merge(
47
- :if_empty => I18n.t("magic_grid.no_results").capitalize, # "No results found."
48
- :searcher_label => I18n.t("magic_grid.search.label").capitalize + ': ', # "Search: "
49
- :searcher_tooltip =>I18n.t("magic_grid.search.tooltip"), # "type.. + <return>"
50
- :searcher_button =>I18n.t("magic_grid.search.button").capitalize, # "Search"
39
+ DEFAULTS.merge(Collection::DEFAULTS)
40
+ .merge(
41
+ if_empty: I18n.t("magic_grid.no_results").capitalize, # "No results found."
42
+ searcher_label: I18n.t("magic_grid.search.label").capitalize + ': ', # "Search: "
43
+ searcher_tooltip: I18n.t("magic_grid.search.tooltip"), # "type.. + <return>"
44
+ searcher_button: I18n.t("magic_grid.search.button").capitalize, # "Search"
51
45
  )
52
46
  end
53
47
 
54
- def initialize(cols_or_opts, collection = nil, controller = nil, opts = {})
48
+ def self.normalize_columns_options(cols_or_opts, opts)
55
49
  if cols_or_opts.is_a? Hash
56
- @options = self.class.runtime_defaults.merge(cols_or_opts.reject {|k| k == :cols})
57
- @columns = cols_or_opts.fetch(:cols, [])
50
+ options = runtime_defaults.merge(cols_or_opts.reject {|k| k == :cols})
51
+ columns = cols_or_opts.fetch(:cols, [])
58
52
  elsif cols_or_opts.is_a? Array
59
- @options = self.class.runtime_defaults.merge opts
60
- @columns = cols_or_opts
53
+ options = runtime_defaults.merge opts
54
+ columns = cols_or_opts
61
55
  else
62
- raise "I have no idea what that is, but it's not a Hash or an Array"
56
+ raise "I have no idea what that is, but it's not a columns list or options hash"
63
57
  end
58
+ [options, columns]
59
+ end
60
+
61
+ def initialize(cols_or_opts, collection = nil, controller = nil, opts = {})
62
+ @options, @columns = *self.class.normalize_columns_options(cols_or_opts, opts)
64
63
  @default_order = @options[:default_order]
65
64
  @params = controller && controller.params || {}
66
- @per_page = @options[:per_page]
67
- @collection = Collection[collection, self]
68
- begin
69
- #if @collection.respond_to? :table
70
- table_name = @collection.quoted_table_name
71
- table_columns = @collection.column_names
72
- rescue
73
- msg = "Given collection doesn't respond to :quoted_table_name or :table well: "
74
- MagicGrid.logger.debug("#{msg} - #{$!}")
75
- table_name = nil
76
- table_columns = @columns.each_index.to_a
77
- end
78
- i = 0
79
- hash = []
80
- @columns.map! do |c|
81
- if c.is_a? Symbol
82
- c = {:col => c}
83
- elsif c.is_a? String
84
- c = {:label => c}
85
- end
86
- c[:id] = i
87
- i += 1
88
- if c.key?(:col) and c[:col].is_a?(Symbol) and table_columns.include?(c[:col])
89
- c[:sql] = "#{table_name}.#{@collection.quote_column_name(c[:col].to_s)}" unless c.key?(:sql)
90
- end
91
- c[:label] = c[:col].to_s.titleize if not c.key? :label
92
- hash << c[:label]
93
- c
94
- end
95
- if @options[:id]
96
- @magic_id = @options[:id]
97
- else
98
- @magic_id = hash.join.hash.abs.to_s(36)
99
- @magic_id << @collection.to_sql.hash.abs.to_s(36) if @collection.respond_to? :to_sql
100
- end
101
- @current_sort_col = sort_col_i = param(:col, @options[:default_col]).to_i
102
- if @collection.sortable? and @columns.count > sort_col_i and @columns[sort_col_i].has_key?(:sql)
103
- sort_col = @columns[sort_col_i][:sql]
104
- @current_order = order(param(:order, @default_order))
105
- sort_dir = order_sql(@current_order)
106
- @collection.apply_sort(sort_col, sort_dir)
107
- else
108
- MagicGrid.logger.debug "#{self.class.name}: Ignoring sorting on non-AR collection"
109
- end
110
65
 
111
- if @collection.filterable? or @options[:listener_handler].respond_to?(:call)
112
- if @options[:listener_handler].respond_to? :call
113
- @collection.apply_filter_callback @options[:listener_handler]
114
- else
115
- @options[:listeners].each_pair do |key, value|
116
- if @params[value] and not @params[value].to_s.empty?
117
- @collection.apply_filter(value => @params[value])
118
- end
119
- end
120
- end
121
- else
122
- unless @options[:listeners].empty?
123
- MagicGrid.logger.warn "#{self.class.name}: Ignoring listener on dumb collection"
124
- @options[:listeners] = {}
125
- end
126
- end
66
+ @collection = Collection.create_or_reuse collection, @options
127
67
 
128
- @options[:searchable] = Array(@options[:searchable])
129
- @options[:current_search] ||= param(:q)
130
- if @collection.searchable?
131
- if param(:q) and not param(:q).empty? and not @options[:searchable].empty?
132
- @collection.apply_search(param(:q))
133
- end
134
- else
135
- if not @options[:searchable].empty? or param(:q)
136
- MagicGrid.logger.warn "#{self.class.name}: Ignoring searchable fields on non-AR collection"
137
- end
138
- @options[:searchable] = []
139
- end
68
+ @columns = Column.columns_for_collection(@collection,
69
+ @columns,
70
+ @options[:searchable])
140
71
 
141
- # Do collection filter first, may convert from AR to Array
142
- if @options[:collection_post_filter?] and @collection.has_post_filter?
143
- @collection.apply_post_filter
144
- end
145
- if @options[:post_filter] and @options[:post_filter].respond_to?(:call)
146
- @collection.apply_filter_callback @options[:post_filter]
72
+ @current_sort_col = param(:col, @options[:default_col]).to_i
73
+ unless (0...@columns.count).cover? @current_sort_col
74
+ @current_sort_col = @options[:default_col]
147
75
  end
148
- # Paginate at the very end, after all sorting, filtering, etc..
149
- @collection.apply_pagination(current_page, @per_page)
76
+ @current_order = order(param(:order, @default_order))
77
+ @collection.apply_sort(@columns[@current_sort_col], order_sql(@current_order))
78
+
79
+ filter_keys = @options[:listeners].values
80
+ filters = @params.slice(*filter_keys).reject {|k,v| v.to_s.empty? }
81
+ @collection.apply_filter filters
82
+ @collection.apply_pagination(current_page)
83
+ @collection.apply_search current_search
84
+
85
+ @collection.per_page = @options[:per_page]
86
+ @collection.apply_filter_callback @options[:listener_handler]
87
+ @collection.enable_post_filter @options[:collection_post_filter]
88
+ @collection.add_post_filter_callback @options[:post_filter]
89
+ end
90
+
91
+ def current_search
92
+ param(:q)
93
+ end
94
+
95
+ def magic_id
96
+ @options[:id] || (Column.hash_string(@columns) + @collection.hash_string)
150
97
  end
151
98
 
152
99
  def searchable?
153
- @collection.searchable? and not @options[:searchable].empty?
100
+ @collection.searchable?
154
101
  end
155
102
 
156
103
  def needs_searcher?
@@ -166,7 +113,7 @@ module MagicGrid
166
113
  end
167
114
 
168
115
  def param_key(key)
169
- "#{@magic_id}_#{key}".to_sym
116
+ "#{magic_id}_#{key}".to_sym
170
117
  end
171
118
 
172
119
  def param(key, default=nil)
@@ -174,7 +121,7 @@ module MagicGrid
174
121
  end
175
122
 
176
123
  def base_params
177
- @params.merge :magic_grid_id => @magic_id
124
+ @params.merge magic_grid_id: magic_id
178
125
  end
179
126
 
180
127
  def current_page