ransack_ui 1.0.1 → 1.0.2

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
@@ -3,6 +3,8 @@
3
3
  Provides HTML templates and JavaScript to build a fully functional
4
4
  advanced search form using Ransack.
5
5
 
6
+ Please note: this project is still in *alpha* and the following instructions are not yet complete/fully working.
7
+
6
8
  ## Installation
7
9
 
8
10
  Add this line to your application's Gemfile:
@@ -17,6 +19,28 @@ Or install it yourself as:
17
19
 
18
20
  $ gem install ransack_ui
19
21
 
22
+ ## Usage
23
+
24
+ Make your model ransackable (if you name associations, it will enable you to search them aswell).
25
+
26
+ ```ruby
27
+ has_ransackable_associations %w(account tags)
28
+ ransack_can_autocomplete
29
+ ```
30
+
31
+ In your controller, where you'd like to add search functionality, add the following before_filter hook. You can change the 'index' action if needed.
32
+
33
+ ```ruby
34
+ before_filter :load_ransack_search, :only => :index
35
+ ```
36
+
37
+ Insert the following helper call into your rails view code where you'd like the search form to appear.
38
+
39
+ ```ruby
40
+ = ransack_ui_search
41
+ ```
42
+
43
+ Now you can start playing with the results.
20
44
 
21
45
  ## Contributing
22
46
 
@@ -25,3 +49,9 @@ Or install it yourself as:
25
49
  3. Commit your changes (`git commit -am 'Add some feature'`)
26
50
  4. Push to the branch (`git push origin my-new-feature`)
27
51
  5. Create new Pull Request
52
+
53
+ ## Credits / Acknowledgements
54
+
55
+ * Nathan Broadbent (ndbroadbent) - creator of ransack_ui code
56
+ * Ernie Miller (ernie) for creating ransack - https://github.com/ernie/ransack
57
+ * Steve Kenworthy (steveyken) - for tiny tweaks
@@ -12,7 +12,8 @@
12
12
 
13
13
  # Store initial predicates and set up Select2 on select lists in .filters
14
14
  containers = el.find('.filters')
15
- @init_select2(containers)
15
+ if Select2?
16
+ @init_select2(containers)
16
17
  @store_initial_predicates(containers)
17
18
 
18
19
  if $.ransack.button_group_select?
@@ -31,7 +32,6 @@
31
32
 
32
33
  base_id = target.attr('id').slice(0, -8)
33
34
  predicate_select = @element.find("select##{base_id}p")
34
- predicate_select2 = @element.find("#s2id_#{base_id}p")
35
35
  query_input = $("input##{base_id}v_0_value")
36
36
 
37
37
  # Initialize datepicker if column is date/datetime/time
@@ -70,14 +70,17 @@
70
70
 
71
71
  # Handle regular columns
72
72
  else
73
- predicate_select2.select2("enable")
74
- # If Select2 is on query input, remove and set defaults
75
- if (query_select2 = @element.find("#s2id_#{base_id}v_0_value")).length > 0
76
- query_input.select2('destroy')
77
- query_input.val('')
78
- previous_val = ''
79
- else
80
- previous_val = predicate_select.val()
73
+ if Select2?
74
+ predicate_select2 = @element.find("#s2id_#{base_id}p")
75
+ predicate_select2.select2("enable")
76
+
77
+ # If Select2 is on query input, remove and set defaults
78
+ if (query_select2 = @element.find("#s2id_#{base_id}v_0_value")).length > 0
79
+ query_input.select2('destroy')
80
+ query_input.val('')
81
+ previous_val = ''
82
+ else
83
+ previous_val = predicate_select.val()
81
84
 
82
85
  # Build array of supported predicates
83
86
  available = predicate_select.data['predicates']
@@ -103,7 +106,8 @@
103
106
  predicate_select.append $("<option value=#{predicate}>#{label}</option>")
104
107
 
105
108
  # Select first predicate if current selection is invalid
106
- predicate_select.select2('val', previous_val)
109
+ if Select2?
110
+ predicate_select.select2('val', previous_val)
107
111
 
108
112
  # Run predicate_changed callback
109
113
  predicate_select.change()
@@ -149,13 +153,15 @@
149
153
  # Dsiables predicate choices and sets it to 'eq'
150
154
  force_eq_predicate: (base_id) ->
151
155
  predicate_select = @element.find("select##{base_id}p")
152
- predicate_select2 = @element.find("#s2id_#{base_id}p")
153
- # Disable predicate Select2
154
- predicate_select2.select2('disable')
155
156
  # Clear predicates, and set 'eq' predicate
156
157
  predicate_select.find('option').each (i, o) -> $(o).remove()
157
158
  predicate_select.append $('<option selected="selected" value="eq">is</option>')
158
- predicate_select2.select2('val', 'eq')
159
+
160
+ if Select2?
161
+ predicate_select2 = @element.find("#s2id_#{base_id}p")
162
+ # Disable predicate Select2
163
+ predicate_select2.select2('disable')
164
+ predicate_select2.select2('val', 'eq')
159
165
 
160
166
  # Attempts to find a predicate translation for the specific column type,
161
167
  # or returns the default label.
@@ -212,14 +218,15 @@
212
218
  # Hide all query inputs
213
219
  inputs.hide()
214
220
 
215
- # Find newly created input and setup Select2
216
- multi_query = @element.find('#' + multi_id)
217
- multi_query.select2
218
- tags: []
219
- tokenSeparators: [',']
220
- formatNoMatches: (t) ->
221
- "Add a search term"
222
- multi_query.select2('val', values)
221
+ if Select2?
222
+ # Find newly created input and setup Select2
223
+ multi_query = @element.find('#' + multi_id)
224
+ multi_query.select2
225
+ tags: []
226
+ tokenSeparators: [',']
227
+ formatNoMatches: (t) ->
228
+ "Add a search term"
229
+ multi_query.select2('val', values)
223
230
 
224
231
  add_query_input: (base_input, base_name, id, value) ->
225
232
  base_input.after $('<input name="'+base_name+'['+id+'][value]" '+
@@ -235,18 +242,18 @@
235
242
  content = target.data("content")
236
243
  new_id = new Date().getTime()
237
244
  regexp = new RegExp('new_' + type, 'g')
238
- container = target.closest('p')
239
- container.before content.replace(regexp, new_id)
240
- prev_container = container.prev()
245
+ target.before content.replace(regexp, new_id)
246
+ prev_container = target.prev()
241
247
 
242
- @init_select2(prev_container)
248
+ if Select2?
249
+ @init_select2(prev_container)
243
250
 
244
251
  if $.ransack.button_group_select?
245
252
  @init_button_group_select(prev_container)
246
253
 
247
254
  @store_initial_predicates(prev_container)
248
255
  # Fire change event on any new selects.
249
- container.prev().find("select").change()
256
+ prev_container.find("select").change()
250
257
  false
251
258
 
252
259
  remove_fields: (e) ->
@@ -269,33 +276,32 @@
269
276
  predicate_select.data['predicates'] = predicates
270
277
 
271
278
  init_select2: (container) ->
272
- if Select2?
273
- container.find('select.ransack_predicate').select2
274
- width: '160px'
275
- formatNoMatches: (term) ->
276
- "Select a field first"
277
-
278
- container.find('select.ransack_attribute').select2
279
- width: '230px'
280
- placeholder: "Select a Field"
281
- allowClear: true
282
- formatSelection: (object, container) ->
283
- # Return 'Model: field' unless column is on root model
284
- if !object.element || $(object.element).data('root-model')
279
+ container.find('select.ransack_predicate').select2
280
+ width: '160px'
281
+ formatNoMatches: (term) ->
282
+ "Select a field first"
283
+
284
+ container.find('select.ransack_attribute').select2
285
+ width: '230px'
286
+ placeholder: "Select a Field"
287
+ allowClear: true
288
+ formatSelection: (object, container) ->
289
+ # Return 'Model: field' unless column is on root model
290
+ if !object.element || $(object.element).data('root-model')
291
+ object.text
292
+ else
293
+ group_label = $(object.element).parent().attr('label')
294
+ # Avoid labels like 'Contact: Contact'
295
+ if group_label == object.text
285
296
  object.text
297
+ else if group_label?
298
+ group_label + ': ' + object.text
286
299
  else
287
- group_label = $(object.element).parent().attr('label')
288
- # Avoid labels like 'Contact: Contact'
289
- if group_label == object.text
290
- object.text
291
- else if group_label?
292
- group_label + ': ' + object.text
293
- else
294
- object.text
295
-
296
- @element.find('select.ransack_sort').select2
297
- width: '230px'
298
- placeholder: "Select a Field"
300
+ object.text
301
+
302
+ @element.find('select.ransack_sort').select2
303
+ width: '230px'
304
+ placeholder: "Select a Field"
299
305
 
300
306
  init_button_group_select: (containers) ->
301
307
  containers.find('select.ransack_combinator, select.ransack_sort_order').button_group_select()
@@ -1,4 +1,4 @@
1
- #ransack_search {
1
+ .ransack_search {
2
2
  .remove_fields, .ransack_attribute, .ransack_predicate,
3
3
  .ransack_query, .ransack_sort_order, .ransack_sort,
4
4
  .btn-group-select {
@@ -15,14 +15,19 @@
15
15
  &:hover { opacity: 0.6; }
16
16
  }
17
17
  .remove_fields.btn {
18
- padding: 0px 3px;
18
+ padding: 0px 3px 2px;
19
19
  &:hover { opacity: 1; }
20
20
  }
21
21
 
22
- a.btn {
23
- i { margin-bottom: 2px; }
22
+ .add_fields.btn {
23
+ margin-top: 20px;
24
+ }
25
+
26
+ .btn {
27
+ i { line-height: 16px; }
24
28
  span { margin-left: 5px; }
25
29
  }
30
+ .btn-small { padding-left: 6px; }
26
31
 
27
32
  .combinator { margin-bottom: 20px; }
28
33
 
@@ -30,4 +35,7 @@
30
35
  margin-top: 20px;
31
36
  }
32
37
 
38
+ .fields.sort {
39
+ margin-bottom: 20px;
40
+ }
33
41
  }
@@ -0,0 +1,15 @@
1
+ <div class="fields condition" data-object-name="<%= f.object_name %>">
2
+ <%= link_to_remove_fields t('ransack.remove_condition'), f, options %>
3
+
4
+ <%= f.attribute_fields do |a| %>
5
+ <span class="fields" data-object-name="<%= f.object_name %>">
6
+ <%= a.attribute_select({}, :class => 'ransack_attribute') %>
7
+ <% end %>
8
+
9
+ <%= f.predicate_select({}, :class => 'ransack_predicate') %>
10
+
11
+ <%= f.value_fields do |v| %>
12
+ <span class="fields value" data-object-name="<%= f.object_name %>">
13
+ <%= v.text_field :value, :style => "width: 200px;", :class => "ransack_query" %>
14
+ <% end %>
15
+ </div>
@@ -0,0 +1,16 @@
1
+ <div class="fields well" data-object-name="<%= f.object_name %>">
2
+ <div class="combinator">
3
+ <% key = (f.object_name =~ /[0]/) ? :group_first : :group_rest %>
4
+ <% combinator = f.combinator_select({}, :class => 'ransack_combinator') %>
5
+ <%= t("ransack.#{key}", :combinator => combinator).html_safe %>
6
+ </div>
7
+
8
+ <div class="filters">
9
+ <% f.object.build_condition unless f.object.conditions.any? %>
10
+ <%= f.condition_fields do |c| %>
11
+ <%= render 'ransack_ui/condition_fields', :f => c, :options => options %>
12
+ <% end %>
13
+
14
+ <%= link_to_add_fields t('ransack.add_condition'), f, :condition, options %>
15
+ </div>
16
+ </div>
@@ -0,0 +1,28 @@
1
+ <%= search_form_for @ransack_search, :url => (options[:url] || url_for(:action => :index)),
2
+ :html => {:method => :get, :class => "ransack_search"}, :remote => !!options[:remote] do |f| %>
3
+
4
+ <% javascript_tag do %>
5
+ if (window.Ransack == null) { window.Ransack = {}; }
6
+ Ransack.alt_predicates_i18n = #{I18n.translate(:"ransack.predicates.alt", :default => {}).to_json}
7
+ <% end %>
8
+
9
+ <div class="row">
10
+ <div class="span12 well">
11
+ <%= f.sort_fields do |s| %>
12
+ <%= render 'ransack_ui/sort_fields', :f => s %>
13
+ <% end %>
14
+
15
+ <%= f.grouping_fields do |g| %>
16
+ <%= render 'ransack_ui/grouping_fields', :f => g, :options => options %>
17
+ <% end %>
18
+
19
+ <%= link_to_add_fields t('ransack.add_group'), f, :grouping, options %>
20
+
21
+ <div class="pull-right">
22
+ <%= hidden_field_tag :distinct, '1' %>
23
+ <%= hidden_field_tag :page, '1' %>
24
+ <%= f.submit t('ransack.submit'), :class => 'btn btn-primary btn-large' %>
25
+ </div>
26
+ </div>
27
+ </div>
28
+ <% end %>
@@ -0,0 +1,4 @@
1
+ <div class="fields sort">
2
+ <span class="sort_by">Sort by:</span>
3
+ <%= f.sort_select %>
4
+ </div>
@@ -12,7 +12,7 @@ module RansackUI
12
12
  ActionController::Base.send :include, ControllerHelpers
13
13
  end
14
14
 
15
- initializer :assets do
15
+ config.before_configuration do
16
16
  # Add images to be precompiled
17
17
  ::Rails.application.config.assets.precompile += %w(ransack_ui/delete.png ransack_ui/calendar.png)
18
18
  end
@@ -1,4 +1,4 @@
1
- require 'ransack/helpers/form_builder'
1
+ require 'ransack/helpers/form_builder'
2
2
 
3
3
  module Ransack
4
4
  module Helpers
@@ -35,10 +35,10 @@ module Ransack
35
35
  @template.select(
36
36
  @object_name, :name,
37
37
  @template.grouped_options_for_select(attribute_collection_for_bases(bases), object.name),
38
- objectify_options(options), @default_options.merge({class: 'ransack_sort'}).merge(html_options)
38
+ objectify_options(options), @default_options.merge({:class => 'ransack_sort'}).merge(html_options)
39
39
  ) + @template.collection_select(
40
40
  @object_name, :dir, [['asc', object.translate('asc')], ['desc', object.translate('desc')]], :first, :last,
41
- objectify_options(options.except(:include_blank)), @default_options.merge({class: 'ransack_sort_order'}).merge(html_options)
41
+ objectify_options(options.except(:include_blank)), @default_options.merge({:class => 'ransack_sort_order'}).merge(html_options)
42
42
  )
43
43
  else
44
44
  # searchable_attributes now returns [c, type]
@@ -50,14 +50,30 @@ module Ransack
50
50
  end
51
51
  @template.collection_select(
52
52
  @object_name, :name, collection, :first, :last,
53
- objectify_options(options), @default_options.merge({class: 'ransack_sort'}).merge(html_options)
53
+ objectify_options(options), @default_options.merge({:class => 'ransack_sort'}).merge(html_options)
54
54
  ) + @template.collection_select(
55
55
  @object_name, :dir, [['asc', object.translate('asc')], ['desc', object.translate('desc')]], :first, :last,
56
- objectify_options(options.except(:include_blank)), @default_options.merge({class: 'ransack_sort_order'}).merge(html_options)
56
+ objectify_options(options.except(:include_blank)), @default_options.merge({:class => 'ransack_sort_order'}).merge(html_options)
57
57
  )
58
58
  end
59
59
  end
60
60
 
61
+ def predicate_keys(options)
62
+ keys = options[:compounds] ? Predicate.names : Predicate.names.reject {|k| k.match(/_(any|all)$/)}
63
+ if only = options[:only]
64
+ if only.respond_to? :call
65
+ keys = keys.select {|k| only.call(k)}
66
+ else
67
+ only = Array.wrap(only).map(&:to_s)
68
+ # Create compounds hash, e.g. {"eq" => ["eq", "eq_any", "eq_all"], "blank" => ["blank"]}
69
+ key_groups = keys.inject(Hash.new([])){ |h,k| h[k.sub(/_(any|all)$/, '')] += [k]; h }
70
+ # Order compounds hash by 'only' keys
71
+ keys = only.map {|k| key_groups[k] }.flatten.compact
72
+ end
73
+ end
74
+ keys
75
+ end
76
+
61
77
  def predicate_select(options = {}, html_options = {})
62
78
  options = Ransack.options[:default_predicates] || {} if options.blank?
63
79
 
@@ -154,5 +170,6 @@ module Ransack
154
170
  nil
155
171
  end
156
172
  end
173
+
157
174
  end
158
- end
175
+ end
@@ -0,0 +1,13 @@
1
+ require 'ransack/nodes/condition'
2
+
3
+ module Ransack
4
+ module Nodes
5
+ Condition.class_eval do
6
+ attr_writer :is_default
7
+
8
+ def default?
9
+ @is_default
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,20 @@
1
+ require 'ransack/nodes/grouping'
2
+
3
+ module Ransack
4
+ module Nodes
5
+ Grouping.class_eval do
6
+
7
+ def new_condition(opts = {})
8
+ attrs = opts[:attributes] || 1
9
+ vals = opts[:values] || 1
10
+ condition = Condition.new(@context)
11
+ condition.predicate_name = opts[:predicate] || 'eq'
12
+ condition.is_default = true
13
+ attrs.times { condition.build_attribute }
14
+ vals.times { condition.build_value }
15
+ condition
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -1,3 +1,3 @@
1
1
  module RansackUI
2
- VERSION = "1.0.1"
2
+ VERSION = "1.0.2"
3
3
  end
data/lib/ransack_ui.rb CHANGED
@@ -6,3 +6,5 @@ require "ransack_chronic"
6
6
  # Require ransack overrides
7
7
  require 'ransack_ui/adapters/active_record'
8
8
  Dir.glob(File.expand_path('../ransack_ui/ransack_overrides/**/*.rb', __FILE__)) {|f| require f }
9
+
10
+ require "ransack"
data/ransack_ui.gemspec CHANGED
@@ -18,5 +18,6 @@ Gem::Specification.new do |gem|
18
18
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
19
  gem.require_paths = ["lib"]
20
20
 
21
- gem.add_dependency 'ransack_chronic'
21
+ gem.add_dependency 'ransack_chronic', '>= 1.1.0'
22
+ gem.add_dependency 'ransack'
22
23
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ransack_ui
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.0.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,10 +9,26 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-12-19 00:00:00.000000000 Z
12
+ date: 2012-12-27 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: ransack_chronic
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 1.1.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: 1.1.0
30
+ - !ruby/object:Gem::Dependency
31
+ name: ransack
16
32
  requirement: !ruby/object:Gem::Requirement
17
33
  none: false
18
34
  requirements:
@@ -48,10 +64,10 @@ files:
48
64
  - app/assets/javascripts/ransack_ui_jquery/search_form.js.coffee.erb
49
65
  - app/assets/stylesheets/ransack_ui_bootstrap/index.css
50
66
  - app/assets/stylesheets/ransack_ui_bootstrap/search.css.scss
51
- - app/views/ransack_ui/_condition_fields.html.haml
52
- - app/views/ransack_ui/_grouping_fields.html.haml
53
- - app/views/ransack_ui/_search.html.haml
54
- - app/views/ransack_ui/_sort_fields.html.haml
67
+ - app/views/ransack_ui/_condition_fields.html.erb
68
+ - app/views/ransack_ui/_grouping_fields.html.erb
69
+ - app/views/ransack_ui/_search.html.erb
70
+ - app/views/ransack_ui/_sort_fields.html.erb
55
71
  - config/locales/en.yml
56
72
  - lib/core_ext/enumerable.rb
57
73
  - lib/ransack_ui.rb
@@ -63,6 +79,8 @@ files:
63
79
  - lib/ransack_ui/ransack_overrides/configuration.rb
64
80
  - lib/ransack_ui/ransack_overrides/context.rb
65
81
  - lib/ransack_ui/ransack_overrides/helpers/form_builder.rb
82
+ - lib/ransack_ui/ransack_overrides/nodes/condition.rb
83
+ - lib/ransack_ui/ransack_overrides/nodes/grouping.rb
66
84
  - lib/ransack_ui/version.rb
67
85
  - lib/ransack_ui/view_helpers.rb
68
86
  - ransack_ui.gemspec
@@ -81,7 +99,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
81
99
  version: '0'
82
100
  segments:
83
101
  - 0
84
- hash: 3453459259470944049
102
+ hash: 1751889683287985587
85
103
  required_rubygems_version: !ruby/object:Gem::Requirement
86
104
  none: false
87
105
  requirements:
@@ -90,7 +108,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
90
108
  version: '0'
91
109
  segments:
92
110
  - 0
93
- hash: 3453459259470944049
111
+ hash: 1751889683287985587
94
112
  requirements: []
95
113
  rubyforge_project:
96
114
  rubygems_version: 1.8.21
@@ -1,13 +0,0 @@
1
- .fields.condition{ "data-object-name" => f.object_name }
2
- %p
3
- = link_to_remove_fields t('ransack.remove_condition'), f, options
4
-
5
- = f.attribute_fields do |a|
6
- %span.fields{ "data-object-name" => f.object_name }
7
- = a.attribute_select({}, :class => 'ransack_attribute')
8
-
9
- = f.predicate_select({}, :class => 'ransack_predicate')
10
-
11
- = f.value_fields do |v|
12
- %span.fields.value{ 'data-object-name' => f.object_name }
13
- = v.text_field :value, :style => "width: 200px;", :class => "ransack_query"
@@ -1,13 +0,0 @@
1
- .fields.well{ 'data-object-name' => f.object_name }
2
- .combinator
3
- - key = (f.object_name =~ /[0]/) ? :group_first : :group_rest
4
- - combinator = f.combinator_select({}, :class => 'ransack_combinator')
5
- = t("ransack.#{key}", :combinator => combinator).html_safe
6
-
7
- .filters
8
- - f.object.build_condition unless f.object.conditions.any?
9
- = f.condition_fields do |c|
10
- = render 'ransack_ui/condition_fields', :f => c, :options => options
11
-
12
- %p
13
- = link_to_add_fields t('ransack.add_condition'), f, :condition, options
@@ -1,23 +0,0 @@
1
- = search_form_for @ransack_search, :url => (options[:url] || url_for(:action => :index)),
2
- :html => {:method => :get, :class => "ransack_search"}, :remote => !!options[:remote] do |f|
3
-
4
- :javascript
5
- if (window.Ransack == null) { window.Ransack = {}; }
6
- Ransack.alt_predicates_i18n = #{I18n.translate(:"ransack.predicates.alt", :default => {}).to_json}
7
-
8
- .row
9
- .span12.well
10
-
11
- = f.sort_fields do |s|
12
- = render 'ransack_ui/sort_fields', :f => s
13
-
14
- = f.grouping_fields do |g|
15
- = render 'ransack_ui/grouping_fields', :f => g, :options => options
16
-
17
- %p
18
- = link_to_add_fields t('ransack.add_group'), f, :grouping, options
19
-
20
- .pull-right
21
- = hidden_field_tag :distinct, '1'
22
- = hidden_field_tag :page, '1'
23
- = f.submit t('ransack.submit'), :class => 'btn btn-primary btn-large'
@@ -1,3 +0,0 @@
1
- .fields.sort
2
- %span.sort_by Sort by:
3
- = f.sort_select