ransack_ui 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ NzUxNDFiZWU1YTllYzkwMTNjZmU2ZTJmZmExOTVlMzI0OWQ4YzQ1Yg==
5
+ data.tar.gz: !binary |-
6
+ NWM3YzRiZWVjMGZjZGY3NTQ2ODg2NzlhZmM2NzlhYzYyNDc0M2Y1Ng==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ OGVhZGIxMDMxZjc3YWQ3YzYwZDk2MjIyOTJiOTZjZmQ2NWQ3MGI5MTQxNTcy
10
+ YjMzNDYwM2U4MDE4NDA1ZDU5OGI0NGNkZGMwMGE2NGNiZGZkODdkZTdlZTc4
11
+ MTEzOGVlOWUzMjc1ODNjZDUzNDFlN2U2NGVkYjI2MzgxMDZiYmQ=
12
+ data.tar.gz: !binary |-
13
+ MDgxY2Y0YzllYTQyMDM1ZGM0NTZkNzI5YWExYTVjNGM2NDhmZDA4NmE4ZjVl
14
+ OTRiMzgzMjEzNjFjYTc1ZWY0MzAzYzIzZjQ0MGZhOGM4NDBjY2NiMGQ2MDVm
15
+ YjdjOTUzOTYxYTZkY2M4MGI0NjBhMjc4ODJhNjRlYTRhMjM5NGE=
@@ -22,61 +22,57 @@
22
22
  # show spinner and disable the form when the search is underway
23
23
  el.find("form input:submit").click $.proxy(@form_submit, this)
24
24
 
25
- # Fire change event for any existing selects.
26
- el.find(".filters select").change()
25
+ # Fire change event for any existing attribute selects,
26
+ # set initialize to true so that existing queries are not cleared.
27
+ el.find(".filters select.ransack_attribute").each (i, el) =>
28
+ @attribute_changed({currentTarget: el}, true)
27
29
 
28
- attribute_changed: (e) ->
30
+ attribute_changed: (e, initialize = false) ->
29
31
  target = $(e.currentTarget)
30
- selected = target.find('option:selected')
31
- column_type = selected.data('type')
32
+ selected_attribute = target.find('option:selected')
33
+ column_type = selected_attribute.data('type')
32
34
 
33
35
  base_id = target.attr('id').slice(0, -8)
34
36
  predicate_select = @element.find("select##{base_id}p")
35
37
  available = predicate_select.data['predicates']
36
38
  query_input = $("input##{base_id}v_0_value")
39
+ multi_id = query_input.attr('id') + '_multi'
40
+ multi_input = @element.find("##{multi_id}")
41
+ query_select2_id = "#s2id_#{base_id}v_0_value"
42
+ query_select2 = @element.find(query_select2_id)
43
+ predicate_select2_id = "#s2id_#{base_id}p"
44
+ predicate_select2 = @element.find(predicate_select2_id)
37
45
 
38
46
  # Initialize datepicker if column is date/datetime/time
39
47
  $.proxy(@init_datetimepicker, this)(base_id)
40
48
 
49
+ # Clear input value unless this is the first run
50
+ unless initialize
51
+ query_input.val('')
52
+
53
+ # Destroy any query select2 inputs on attribute change and clear input
54
+ if query_select2.length
55
+ query_input.select2('destroy')
56
+
57
+ # Destroy any multi-inputs on attribute change and clear input
58
+ if multi_input.length
59
+ @destroy_multi_input(multi_input, selected_attribute.val())
60
+
41
61
  # Handle association columns with AJAX autocomplete
42
- if selected.data('ajax-url') and Select2?
43
- controller = selected.data('controller')
62
+ if selected_attribute.data('ajax-url') and Select2?
44
63
  @set_option_predicates(base_id, available, column_type)
45
64
 
46
- # Set up Select2 for query input
47
- query_input.val('')
48
- query_input.select2
49
- placeholder: "Search #{selected.data('ajax-entity')}"
50
- minimumInputLength: 1
51
- allowClear: true
52
- ajax:
53
- url: selected.data('ajax-url')
54
- dataType: 'json'
55
- type: selected.data('ajax-type')
56
- data: (query, page) ->
57
- obj = {}
58
- obj[selected.data('ajax-key')] = query
59
- obj
60
- results: (data, page) ->
61
- {results: $.map(data, (text, id) -> {id: id, text: text}) }
62
-
63
65
  # Handle columns with options detected from validates :inclusion
64
- else if selected.data('select-options') and Select2?
65
- @set_option_predicates(base_id, available, column_type)
66
- query_input.val('')
67
- query_input.select2
68
- data: selected.data('select-options')
69
- placeholder: "Please select a #{selected.val()}"
70
- allowClear: true
66
+ else if selected_attribute.data('select-options') and Select2?
67
+ @set_option_predicates(base_id, available, column_type, true)
71
68
 
72
69
  # Handle regular columns
73
70
  else
74
71
  if Select2?
75
- predicate_select2 = @element.find("#s2id_#{base_id}p")
76
72
  predicate_select2.select2("enable")
77
73
 
78
74
  # If Select2 is on query input, remove and set defaults
79
- if (query_select2 = @element.find("#s2id_#{base_id}v_0_value")).length > 0
75
+ if query_select2.length > 0
80
76
  query_input.select2('destroy')
81
77
  query_input.val('')
82
78
  previous_val = ''
@@ -109,8 +105,8 @@
109
105
  if Select2?
110
106
  predicate_select.select2('val', previous_val)
111
107
 
112
- # Run predicate_changed callback
113
- predicate_select.change()
108
+ # Run predicate_changed callback
109
+ predicate_select.change()
114
110
 
115
111
  return true
116
112
 
@@ -124,86 +120,111 @@
124
120
  attribute_select = @element.find("select##{base_id}a_0_name")
125
121
  selected_attribute = attribute_select.find('option:selected')
126
122
 
123
+ query_select2_id = "#s2id_#{base_id}v_0_value"
124
+ query_select2 = @element.find(query_select2_id)
125
+ query_select2_multi_id = "#s2id_#{base_id}v_0_value_multi"
126
+ query_select2_multi = @element.find(query_select2_multi_id)
127
+
128
+ no_query_predicates = ["true", "false", "blank", "present", "null", "not_null"]
129
+
127
130
  # We need to use a dummy input to handle multiple terms
128
131
  multi_id = query_input.attr('id') + '_multi'
129
132
  multi_input = @element.find("##{multi_id}")
130
133
 
134
+ # If query was previously hidden, clear query input
135
+ if query_select2.length == 0 && multi_input.length == 0 && query_input.is(":hidden")
136
+ query_input.val('')
137
+
138
+ # Hide query input when not needed
139
+ if p in no_query_predicates
140
+ # If Select2 is on query input, remove and set defaults
141
+ if Select2? && query_select2.length
142
+ query_input.select2('destroy')
143
+
144
+ query_input.val("true")
145
+ query_input.hide()
146
+ query_input.parent().find('.ui-datepicker-trigger').hide()
147
+
131
148
  if Select2?
132
149
  # Turn query input into Select2 tag list when query accepts multiple values
133
150
  if p in ["in", "not_in"] || p.match(/_(all|any)$/)
134
- # If Select2 is on query input, remove and set defaults
135
- if @element.find("#s2id_#{base_id}v_0_value").length
136
- query_input.select2('destroy')
137
-
138
151
  # Add dummy 'multi' input for select2 if not already added
139
- if multi_input.length == 0
152
+ if multi_input.length == 0 && query_select2_multi.length == 0
140
153
  # Set up multi-query input with fixed options, if present
141
- query_input.val('')
142
- options = selected_attribute.data('select-options')
143
- @setup_multi_query_input(target, query_input, multi_id, options)
154
+ @setup_multi_query_input(target, query_input, multi_id, selected_attribute)
155
+
156
+ # If Select2 is on query input, remove and set defaults
157
+ if query_select2.length
158
+ query_input.select2('destroy').hide()
144
159
 
145
160
  return
146
161
 
147
- else
162
+ else
163
+ # Otherwise, remove Select2 from multi-query input, and remove input.
148
164
  if multi_input.length
149
- # Otherwise, remove Select2 from multi-query input, and remove input.
150
- multi_input.select2('destroy').remove()
151
- # Also remove all extra inputs
152
- base_name = multi_input.data('base-name')
153
- inputs = @element.find("input[name^=\"#{base_name}\"]")
154
- inputs = inputs.slice(1)
155
- inputs.remove()
156
-
157
- # If no fixed options, show query input
158
- query_input.val('').show()
159
-
160
- # Handle fixed options - set up Select2 for single values if not already present
161
- if selected_attribute.data('select-options')
162
- if (query_select2 = @element.find("#s2id_#{base_id}v_0_value")).length == 0
163
- query_input.select2
164
- data: selected_attribute.data('select-options')
165
- placeholder: "Please select a #{selected_attribute.val()}"
166
- allowClear: true
165
+ # Save label data from first value
166
+ if multi_input.select2('data') && multi_input.select2('data').length
167
+ multi_input_data = multi_input.select2('data').first()
168
+ Ransack.value_field_labels[selected_attribute.val()] ||= {}
169
+ Ransack.value_field_labels[selected_attribute.val()][multi_input_data.id] = multi_input_data.text
167
170
 
168
- return
171
+ @destroy_multi_input(multi_input, selected_attribute.val())
169
172
 
173
+ if p not in no_query_predicates
174
+ query_input.show()
170
175
 
171
- # Hide query input when not needed
172
- if p in ["true", "false", "blank", "present", "null", "not_null"]
173
- # If Select2 is on query input, remove and set defaults
174
- if Select2? && @element.find("#s2id_#{base_id}v_0_value").length
175
- query_input.select2('destroy')
176
- query_input.val('')
176
+ # Handle association columns with AJAX autocomplete
177
+ if selected_attribute.data('ajax-url')
178
+ if query_select2.length
179
+ query_input.hide()
180
+ else
181
+ @setup_select2_association(query_input, selected_attribute)
182
+
183
+ # Handle fixed options - set up Select2 for single values if not already present
184
+ if selected_attribute.data('select-options')
185
+ if query_select2.length
186
+ query_input.hide()
187
+ else
188
+ @setup_select2_options(query_input, selected_attribute)
177
189
 
178
- query_input.val("true")
179
- query_input.hide()
180
- query_input.parent().find('.ui-datepicker-trigger').hide()
181
190
  # Otherwise, reset query input and show datepicker trigger if present
182
- else
183
- unless query_input.is(":visible")
184
- query_input.val("")
185
- query_input.show()
186
- query_input.parent().find('.ui-datepicker-trigger').show()
191
+ if p not in no_query_predicates
192
+ return if selected_attribute.data('select-options')
193
+
194
+ # Don't show query input if ajax auto complete is present on selected attribute
195
+ unless p in ['eq', 'not_eq'] and selected_attribute.data('ajax-url')
196
+ unless query_input.is(":visible")
197
+ query_input.val('')
198
+ query_input.show()
199
+ query_input.parent().find('.ui-datepicker-trigger').show()
187
200
 
188
201
 
189
- # Dsiables predicate choices and sets it to 'eq'
190
- set_option_predicates: (base_id, available, column_type) ->
202
+ # Disables predicate choices and sets it to 'eq'
203
+ set_option_predicates: (base_id, available_predicates, column_type, include_number_predicates = false) ->
191
204
  predicate_select = @element.find("select##{base_id}p")
205
+ previous_val = predicate_select.val()
192
206
 
193
207
  # Remove all predicates, and add any supported predicates
194
208
  predicate_select.find('option').each (i, o) -> $(o).remove()
195
209
 
196
- $.each available, (i, p) =>
210
+ allowed_predicates = $.merge([], Ransack.option_predicates)
211
+
212
+ # Include number predicates if the option was given.
213
+ # For example, a integer column will have fixed select options,
214
+ # but will also allow less than and greater than.
215
+ if column_type in ['integer', 'float', 'decimal'] && include_number_predicates
216
+ allowed_predicates = allowed_predicates.concat(Ransack.type_predicates[column_type] || [])
217
+
218
+ $.each available_predicates, (i, p) =>
197
219
  [predicate, label] = [p[0], p[1]]
198
220
 
199
- if predicate in Ransack.option_predicates
221
+ if predicate in allowed_predicates
200
222
  # Get alternative predicate label depending on column type
201
223
  label = @alt_predicate_label_or_default(predicate, column_type, label)
202
224
  predicate_select.append $("<option value=#{predicate}>#{label}</option>")
203
225
 
204
- if Select2?
205
- predicate_select2 = @element.find("#s2id_#{base_id}p")
206
- predicate_select2.select2('val', 'eq')
226
+ # Select first predicate if current selection is invalid
227
+ predicate_select.select2('val', previous_val)
207
228
 
208
229
  # Attempts to find a predicate translation for the specific column type,
209
230
  # or returns the default label.
@@ -246,17 +267,20 @@
246
267
  $.each values, (i, v) =>
247
268
  @add_query_input(target, base_name, i + 1, v)
248
269
 
249
- setup_multi_query_input: (predicate_el, query_el, multi_id, options) ->
270
+ setup_multi_query_input: (predicate_el, query_input, multi_id, selected_attribute) ->
250
271
  base_name = predicate_el.attr('name').slice(0, -3) + '[v]'
251
- query_el.after(
272
+ base_id = predicate_el.attr('id').slice(0, -1)
273
+ query_input.after(
252
274
  $('<input class="ransack_query_multi" id="' + multi_id + '" ' +
253
- 'style="width:' + (query_el.width() * 2) + 'px;" ' +
275
+ 'style="width:' + (query_input.width() * 2) + 'px;" ' +
254
276
  'data-base-name="' + base_name + '" />'))
255
277
 
278
+ query_select2_id = "#s2id_#{base_id}v_0_value"
279
+ query_select2 = @element.find(query_select2_id)
280
+
256
281
  # Fetch all existing values
257
282
  inputs = @element.find("input[name^=\"#{base_name}\"]")
258
- values = $.map inputs, (el) ->
259
- el.value
283
+ values = $.map inputs, (el) -> el.value
260
284
  # Hide all query inputs
261
285
  inputs.hide()
262
286
 
@@ -264,37 +288,102 @@
264
288
  # Find newly created input and setup Select2
265
289
  multi_query = @element.find('#' + multi_id)
266
290
 
267
- if options?
268
- # Setup Select2 with fixed options
269
- multi_query.select2
270
- data: options
271
- multiple: true
272
- tokenSeparators: [',']
273
- formatNoMatches:
274
- if options
275
- (t) -> "No matches found."
276
- else
277
- (t) -> "Add a search term"
278
- initSelection: (element, callback) ->
279
- data = []
280
- unless element.val().trim() == ""
281
- $(element.val().split(",")).each ->
282
- data.push {id: this, text: this}
283
- callback(data)
291
+ # Handle association columns with AJAX autocomplete
292
+ if selected_attribute.data('ajax-url')
293
+ # Set label to single association label, if anything was selected
294
+ if query_select2.length && query_input.select2('data')
295
+ query_input_data = query_input.select2('data')
296
+ Ransack.value_field_labels[selected_attribute.val()] ||= {}
297
+ Ransack.value_field_labels[selected_attribute.val()][query_input_data.id] = query_input_data.text
298
+
299
+ @setup_select2_association(multi_query, selected_attribute, true)
300
+
284
301
  else
285
- # Setup Select2 with tagging support (can create options)
286
- multi_query.select2
287
- tags: []
288
- tokenSeparators: [',']
289
- formatNoMatches: (t) ->
290
- "Add a search term"
302
+ if selected_attribute.data('select-options')
303
+ # Setup Select2 with fixed multiple options
304
+ @setup_select2_options(multi_query, selected_attribute, true)
305
+
306
+ else
307
+ # Setup Select2 with tagging support (can create options)
308
+ multi_query.select2
309
+ tags: []
310
+ tokenSeparators: [',']
311
+ formatNoMatches: (t) ->
312
+ "Add a search term"
291
313
 
292
314
  multi_query.select2('val', values)
293
315
 
316
+ setup_select2_association: (query_input, selected_attribute, multiple = false) ->
317
+ selected_attribute_val = selected_attribute.val()
318
+ # Set up Select2 for query input
319
+ query_input.select2
320
+ placeholder: "Search #{selected_attribute.data('ajax-entity')}"
321
+ minimumInputLength: 1
322
+ allowClear: true
323
+ multiple: multiple
324
+ ajax:
325
+ url: selected_attribute.data('ajax-url')
326
+ dataType: 'json'
327
+ type: selected_attribute.data('ajax-type')
328
+ data: (query, page) ->
329
+ obj = {}
330
+ obj[selected_attribute.data('ajax-key')] = query
331
+ obj
332
+ results: (data, page) ->
333
+ {results: $.map(data, (text, id) -> {id: id, text: text}) }
334
+ initSelection: (element, callback) ->
335
+ data = []
336
+ unless element.val().trim() == ""
337
+ $(element.val().split(",")).each (i, val) ->
338
+ label = Ransack.value_field_labels[selected_attribute_val]?[val]
339
+ if label
340
+ data.push {id: val, text: label}
341
+ else
342
+ data.push {id: val, text: val}
343
+ if data.length
344
+ callback(multiple and data or data[0])
345
+ else
346
+ # If no label could be found, clear value
347
+ element.select2('val', '')
348
+
349
+ setup_select2_options: (query_input, selected_attribute, multiple = false) ->
350
+ query_input.select2
351
+ data: selected_attribute.data('select-options')
352
+ placeholder: "Please select a #{selected_attribute.text()}"
353
+ allowClear: true
354
+ multiple: multiple
355
+ tokenSeparators: [',']
356
+ formatNoMatches:
357
+ if selected_attribute.data('select-options')
358
+ (t) -> "No matches found."
359
+ else
360
+ (t) -> "Add a search term"
361
+ initSelection: (element, callback) ->
362
+ data = []
363
+ unless element.val().trim() == ""
364
+ $(element.val().split(",")).each (i, val) ->
365
+ selected_attribute.data('select-options').each (option, i) ->
366
+ if option.id == val
367
+ data.push {id: option.id, text: option.text}
368
+ return false # Break out of inner each loop
369
+
370
+ if data.length
371
+ callback(multiple and data or data[0])
372
+ else
373
+ element.select2('val', '')
374
+
294
375
  add_query_input: (base_input, base_name, id, value) ->
295
376
  base_input.after $('<input name="'+base_name+'['+id+'][value]" '+
296
377
  'value="'+value+'" style="display:none;" />')
297
378
 
379
+ destroy_multi_input: (multi_input, selected_attribute_val) ->
380
+ multi_input.select2('destroy').remove()
381
+ # Also remove all extra inputs
382
+ base_name = multi_input.data('base-name')
383
+ inputs = @element.find("input[name^=\"#{base_name}\"]")
384
+ inputs = inputs.slice(1)
385
+ inputs.remove()
386
+
298
387
  form_submit: (e) ->
299
388
  @element.css({ opacity: 0.4 })
300
389
  true
@@ -330,7 +419,7 @@
330
419
 
331
420
  store_initial_predicates: (container) ->
332
421
  # Store current predicates in data attribute
333
- predicate_select = container.find('select.ransack_predicate:first')
422
+ predicate_select = container.find('select.ransack_predicate').first()
334
423
  unless predicate_select.data['predicates']
335
424
  predicates = []
336
425
  predicate_select.find('option').each (i, o) ->
@@ -349,8 +438,16 @@
349
438
  placeholder: "Select a Field"
350
439
  allowClear: true
351
440
  formatSelection: (object, container) ->
441
+ # If initializing and element is not present,
442
+ # search for option element in original select tag
443
+ if !object.element
444
+ this.element.find('option').each (i, option) ->
445
+ if option.value == object.id
446
+ object.element = option
447
+ return false
448
+
352
449
  # Return 'Model: field' unless column is on root model
353
- if !object.element || $(object.element).data('root-model')
450
+ if $(object.element).data('root-model')
354
451
  object.text
355
452
  else
356
453
  group_label = $(object.element).parent().attr('label')
@@ -372,7 +469,7 @@
372
469
  init_datetimepicker: (base_id) ->
373
470
  if $.ui?.timepicker?
374
471
  query_input = @element.find("input##{base_id}v_0_value")
375
- selected_attr = @element.find("select##{base_id}a_0_name option:selected")
472
+ selected_attribute = @element.find("select##{base_id}a_0_name option:selected")
376
473
 
377
474
  # Clear any datepicker from query input first
378
475
  query_input.datepicker('destroy')
@@ -388,7 +485,7 @@
388
485
  onClose: (date) -> $(this).val(date)
389
486
 
390
487
  # Show datepicker button for dates
391
- switch selected_attr.data('type')
488
+ switch selected_attribute.data('type')
392
489
  when "date"
393
490
  query_input.datepicker(datepicker_options)
394
491
  when "datetime"
@@ -1,9 +1,10 @@
1
1
  <%= search_form_for @ransack_search, :url => (options[:url] || url_for(:action => :index)),
2
2
  :html => {:method => :get, :class => "ransack_search"}, :remote => !!options[:remote] do |f| %>
3
3
 
4
- <% javascript_tag do %>
4
+ <%= javascript_tag do %>
5
5
  if (window.Ransack == null) { window.Ransack = {}; }
6
- Ransack.alt_predicates_i18n = #{I18n.translate(:"ransack.predicates.alt", :default => {}).to_json}
6
+ Ransack.alt_predicates_i18n = <%= I18n.translate(:"ransack.predicates.alt", :default => {}).to_json.html_safe %>;
7
+ Ransack.value_field_labels = <%= f.labels_for_value_fields.to_json.html_safe %>
7
8
  <% end %>
8
9
 
9
10
  <div class="row">
@@ -21,7 +22,7 @@
21
22
  <div class="pull-right">
22
23
  <%= hidden_field_tag :distinct, '1' %>
23
24
  <%= hidden_field_tag :page, '1' %>
24
- <%= f.submit t('ransack.submit'), :class => 'btn btn-primary btn-large' %>
25
+ <%= f.submit t('ransack.submit'), :class => 'btn btn-primary btn-large submit-search' %>
25
26
  </div>
26
27
  </div>
27
28
  </div>
@@ -3,6 +3,9 @@ require 'ransack/helpers/form_builder'
3
3
  module Ransack
4
4
  module Helpers
5
5
  FormBuilder.class_eval do
6
+ cattr_accessor :cached_searchable_attributes_for_base
7
+ self.cached_searchable_attributes_for_base = {}
8
+
6
9
  def attribute_select(options = {}, html_options = {})
7
10
  raise ArgumentError, "attribute_select must be called inside a search FormBuilder!" unless object.respond_to?(:context)
8
11
  options[:include_blank] = true unless options.has_key?(:include_blank)
@@ -58,6 +61,44 @@ module Ransack
58
61
  end
59
62
  end
60
63
 
64
+ def labels_for_value_fields
65
+ labels = {}
66
+
67
+ object.groupings.each do |grouping|
68
+ grouping.conditions.each do |condition|
69
+ condition.values.each do |value|
70
+ # If value is present, and the attribute is an association,
71
+ # load the selected record and include the record name as a data attribute
72
+ if value.value.present?
73
+ condition_attributes = condition.attributes
74
+ if condition_attributes.any?
75
+ attribute = condition_attributes.first.name
76
+ klass_name = foreign_klass_for_attribute(attribute)
77
+
78
+ if klass_name
79
+ klass = klass_name.constantize
80
+
81
+ value_object = klass.find_by_id(value.value)
82
+ if value_object
83
+ labels[attribute] ||= {}
84
+
85
+ if value_object.respond_to? :full_name
86
+ labels[attribute][value.value] = value_object.full_name
87
+ elsif value_object.respond_to? :name
88
+ labels[attribute][value.value] = value_object.name
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+
98
+ labels
99
+ end
100
+
101
+
61
102
  def predicate_keys(options)
62
103
  keys = options[:compounds] ? Predicate.names : Predicate.names.reject {|k| k.match(/_(any|all)$/)}
63
104
  if only = options[:only]
@@ -101,9 +142,6 @@ module Ransack
101
142
 
102
143
  def attribute_collection_for_base(base)
103
144
  klass = object.context.traverse(base)
104
- foreign_keys = klass.reflect_on_all_associations.select(&:belongs_to?).
105
- map_to({}) {|r, h| h[r.foreign_key.to_sym] = r.class_name }
106
-
107
145
  ajax_options = Ransack.options[:ajax_options] || {}
108
146
 
109
147
  # Detect any inclusion validators to build list of options for a column
@@ -112,41 +150,36 @@ module Ransack
112
150
  v.attributes.each do |a|
113
151
  # Try to translate options from activerecord.attribute_options.<model>.<attribute>
114
152
  hash[a.to_s] = v.send(:delimiter).each_with_object({}) do |o, options|
115
- options[o] = I18n.translate("activerecord.attribute_options.#{klass.to_s.downcase}.#{a}.#{o}", :default => o)
153
+ options[o.to_s] = I18n.translate("activerecord.attribute_options.#{klass.to_s.downcase}.#{a}.#{o}", :default => o.to_s.titleize)
116
154
  end
117
155
  end
118
156
  end
119
157
  end
120
158
 
121
- object.context.searchable_attributes(base).map do |c, type|
122
- # Don't show 'id' column for base model
123
- next nil if base.blank? && c == 'id'
159
+ if klass.respond_to?(:ransack_column_select_options)
160
+ column_select_options.merge!(klass.ransack_column_select_options)
161
+ end
124
162
 
125
- attribute = attr_from_base_and_column(base, c)
126
- attribute_label = Translate.attribute(attribute, :context => object.context)
163
+ searchable_attributes_for_base(base).map do |attribute_data|
164
+ column = attribute_data[:column]
127
165
 
128
- # Set model name as label for 'id' column on that model's table.
129
- if c == 'id'
130
- foreign_klass = object.context.traverse(base).model_name
131
- # Check that model can autocomplete. If not, skip this id column.
132
- next nil unless foreign_klass.constantize._ransack_can_autocomplete
133
- attribute_label = I18n.translate(foreign_klass, :default => foreign_klass)
134
- else
135
- foreign_klass = foreign_keys[c.to_sym]
136
- end
166
+ html_options = {}
137
167
 
138
168
  # Add column type as data attribute
139
- html_options = {:'data-type' => type}
169
+ html_options[:'data-type'] = attribute_data[:type]
140
170
  # Set 'base' attribute if attribute is on base model
141
171
  html_options[:'data-root-model'] = true if base.blank?
172
+
142
173
  # Set column options if detected from inclusion validator
143
- if column_select_options[c]
174
+ if column_select_options[column]
144
175
  # Format options as an array of hashes with id and text columns, for Select2
145
- html_options[:'data-select-options'] = column_select_options[c].map {|id, text|
176
+ html_options[:'data-select-options'] = column_select_options[column].map {|id, text|
146
177
  {:id => id, :text => text}
147
178
  }.to_json
148
179
  end
149
180
 
181
+ foreign_klass = attribute_data[:foreign_klass]
182
+
150
183
  if foreign_klass
151
184
  # If field is a foreign key, set up 'data-ajax-*' attributes for auto-complete
152
185
  controller = foreign_klass.tableize
@@ -161,15 +194,66 @@ module Ransack
161
194
  end
162
195
 
163
196
  [
164
- attribute_label,
165
- attribute,
197
+ attribute_data[:label],
198
+ attribute_data[:attribute],
166
199
  html_options
167
200
  ]
168
- end.compact
201
+ end
169
202
  rescue UntraversableAssociationError => e
170
203
  nil
171
204
  end
172
- end
173
205
 
206
+
207
+ private
208
+
209
+ def searchable_attributes_for_base(base)
210
+ cache_prefix = object.context.klass.table_name
211
+ cache_key = base.blank? ? cache_prefix : [cache_prefix, base].join('_')
212
+
213
+ self.class.cached_searchable_attributes_for_base[cache_key] ||= object.context.searchable_attributes(base).map do |column, type|
214
+ klass = object.context.traverse(base)
215
+ foreign_keys = klass.reflect_on_all_associations.select(&:belongs_to?).
216
+ each_with_object({}) {|r, h| h[r.foreign_key.to_sym] = r.class_name }
217
+
218
+ # Don't show 'id' column for base model
219
+ next nil if base.blank? && column == 'id'
220
+
221
+ attribute = attr_from_base_and_column(base, column)
222
+ attribute_label = Translate.attribute(attribute, :context => object.context)
223
+
224
+ # Set model name as label for 'id' column on that model's table.
225
+ if column == 'id'
226
+ foreign_klass = object.context.traverse(base).model_name
227
+ # Check that model can autocomplete. If not, skip this id column.
228
+ next nil unless foreign_klass.constantize._ransack_can_autocomplete
229
+ attribute_label = I18n.translate(foreign_klass, :default => foreign_klass)
230
+ else
231
+ foreign_klass = foreign_keys[column.to_sym]
232
+ end
233
+
234
+ attribute_data = {
235
+ label: attribute_label,
236
+ type: type,
237
+ column: column,
238
+ attribute: attribute
239
+ }
240
+ attribute_data[:foreign_klass] = foreign_klass if foreign_klass
241
+ attribute_data
242
+ end.compact
243
+ end
244
+
245
+ def foreign_klass_for_attribute(attribute)
246
+ associations = object.context.klass.ransackable_associations
247
+ bases = [''] + association_array(associations)
248
+
249
+ bases.each do |base|
250
+ searchable_attributes_for_base(base).each do |attribute_data|
251
+ if attribute == attribute_data[:attribute]
252
+ return attribute_data[:foreign_klass]
253
+ end
254
+ end
255
+ end
256
+ end
257
+ end
174
258
  end
175
259
  end
@@ -1,3 +1,3 @@
1
1
  module RansackUI
2
- VERSION = "1.1.0"
2
+ VERSION = "1.2.0"
3
3
  end
data/lib/ransack_ui.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  require "ransack_ui/version"
2
2
  require "ransack_ui/rails/engine"
3
- require "core_ext/enumerable"
4
3
  require "ransack_chronic"
5
4
 
6
5
  # Require ransack overrides
metadata CHANGED
@@ -1,20 +1,18 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ransack_ui
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
5
- prerelease:
4
+ version: 1.2.0
6
5
  platform: ruby
7
6
  authors:
8
7
  - Nathan Broadbent
9
8
  autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2013-01-21 00:00:00.000000000 Z
11
+ date: 2013-12-12 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: ransack_chronic
16
15
  requirement: !ruby/object:Gem::Requirement
17
- none: false
18
16
  requirements:
19
17
  - - ! '>='
20
18
  - !ruby/object:Gem::Version
@@ -22,7 +20,6 @@ dependencies:
22
20
  type: :runtime
23
21
  prerelease: false
24
22
  version_requirements: !ruby/object:Gem::Requirement
25
- none: false
26
23
  requirements:
27
24
  - - ! '>='
28
25
  - !ruby/object:Gem::Version
@@ -30,7 +27,6 @@ dependencies:
30
27
  - !ruby/object:Gem::Dependency
31
28
  name: ransack
32
29
  requirement: !ruby/object:Gem::Requirement
33
- none: false
34
30
  requirements:
35
31
  - - ! '>='
36
32
  - !ruby/object:Gem::Version
@@ -38,7 +34,6 @@ dependencies:
38
34
  type: :runtime
39
35
  prerelease: false
40
36
  version_requirements: !ruby/object:Gem::Requirement
41
- none: false
42
37
  requirements:
43
38
  - - ! '>='
44
39
  - !ruby/object:Gem::Version
@@ -69,7 +64,6 @@ files:
69
64
  - app/views/ransack_ui/_search.html.erb
70
65
  - app/views/ransack_ui/_sort_fields.html.erb
71
66
  - config/locales/en.yml
72
- - lib/core_ext/enumerable.rb
73
67
  - lib/ransack_ui.rb
74
68
  - lib/ransack_ui/adapters/active_record.rb
75
69
  - lib/ransack_ui/adapters/active_record/base.rb
@@ -87,32 +81,25 @@ files:
87
81
  homepage: https://github.com/ndbroadbent/ransack_ui
88
82
  licenses:
89
83
  - MIT
84
+ metadata: {}
90
85
  post_install_message:
91
86
  rdoc_options: []
92
87
  require_paths:
93
88
  - lib
94
89
  required_ruby_version: !ruby/object:Gem::Requirement
95
- none: false
96
90
  requirements:
97
91
  - - ! '>='
98
92
  - !ruby/object:Gem::Version
99
93
  version: '0'
100
- segments:
101
- - 0
102
- hash: 3625052068884160403
103
94
  required_rubygems_version: !ruby/object:Gem::Requirement
104
- none: false
105
95
  requirements:
106
96
  - - ! '>='
107
97
  - !ruby/object:Gem::Version
108
98
  version: '0'
109
- segments:
110
- - 0
111
- hash: 3625052068884160403
112
99
  requirements: []
113
100
  rubyforge_project:
114
- rubygems_version: 1.8.24
101
+ rubygems_version: 2.1.11
115
102
  signing_key:
116
- specification_version: 3
103
+ specification_version: 4
117
104
  summary: UI Builder for Ransack
118
105
  test_files: []
@@ -1,3 +0,0 @@
1
- module Enumerable
2
- alias map_to each_with_object
3
- end