magic_grid 0.11.1 → 0.12.0

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/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