JasonKing-good_sort 0.1.1
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/LICENSE +20 -0
- data/README.markdown +144 -0
- data/VERSION.yml +4 -0
- data/lib/good_sort/sorter.rb +53 -0
- data/lib/good_sort/view_helpers.rb +74 -0
- data/lib/good_sort/will_paginate.rb +14 -0
- data/lib/good_sort.rb +18 -0
- data/test/good_sort_test.rb +83 -0
- data/test/test_helper.rb +7 -0
- metadata +64 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Jason King
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.markdown
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
Good Sort
|
2
|
+
=========
|
3
|
+
|
4
|
+
Hate not having _the right way_™ to do column sorting in list (collection)
|
5
|
+
views? Well, fear not my dear friend, for good_sort has arrived. Does Ajax for
|
6
|
+
those with JS and regular links for those without. Also, as long as you load it
|
7
|
+
after will_paginate, it will sniff it out and make the will_paginate view helper
|
8
|
+
do the right thing with your sorted collection.
|
9
|
+
|
10
|
+
Installation
|
11
|
+
------------
|
12
|
+
|
13
|
+
### gem
|
14
|
+
|
15
|
+
To perform a system wide installation:
|
16
|
+
|
17
|
+
gem source -a http://gems.github.com
|
18
|
+
gem install JasonKing-good_sort
|
19
|
+
|
20
|
+
Then add it to your `config/environment.rb`:
|
21
|
+
|
22
|
+
config.gem 'JasonKing-good_sort', :lib => 'good_sort'
|
23
|
+
|
24
|
+
### plugin
|
25
|
+
|
26
|
+
script/plugin install git://github.com/JasonKing/good_sort.git
|
27
|
+
|
28
|
+
### git submodule
|
29
|
+
|
30
|
+
$ git submodule add git://github.com/JasonKing/good_sort.git vendor/plugins/good_sort
|
31
|
+
|
32
|
+
Usage
|
33
|
+
-----
|
34
|
+
|
35
|
+
### app/models/author.rb
|
36
|
+
sort_on :name, :updated_at
|
37
|
+
|
38
|
+
### app/controllers/site_controller.rb
|
39
|
+
def index
|
40
|
+
@authors = Author.all( Author.sort_by(params[:sort]) )
|
41
|
+
|
42
|
+
if request.xhr?
|
43
|
+
return render :partial => 'authors'
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
### app/views/site/index.html.erb
|
48
|
+
<div id="authors">
|
49
|
+
<%= render :partial => 'authors' %>
|
50
|
+
</div>
|
51
|
+
|
52
|
+
### app/views/site/_authors.html.erb
|
53
|
+
<table>
|
54
|
+
<thead>
|
55
|
+
<tr>
|
56
|
+
<%
|
57
|
+
sort_headers_for :author, %w{name ranking phone updated_at} do |header|
|
58
|
+
"Last Changed" if header == 'updated_at'
|
59
|
+
end
|
60
|
+
%>
|
61
|
+
</tr>
|
62
|
+
</thead>
|
63
|
+
<tbody>
|
64
|
+
<% @authors.each do |author| -%>
|
65
|
+
<tr>
|
66
|
+
<td><%=h author.name %></td>
|
67
|
+
<td><%=h author.ranking %></td>
|
68
|
+
<td><%=h author.phone %></td>
|
69
|
+
<td><%=h author.updated_at %></td>
|
70
|
+
</tr>
|
71
|
+
<% end -%>
|
72
|
+
</tbody>
|
73
|
+
</table>
|
74
|
+
|
75
|
+
That's simple enough isn't it?
|
76
|
+
|
77
|
+
The `sort_headers_for` helper will make a heading for each one of the elements
|
78
|
+
in the array you pass in - if it's one of the fields that you've set sorting on
|
79
|
+
in your model (using the `sort_on` class method).
|
80
|
+
|
81
|
+
Methods
|
82
|
+
-------
|
83
|
+
|
84
|
+
### ActiveRecord::Base.sort\_on( *args )
|
85
|
+
|
86
|
+
This is the class method that you use in your model in order to let `good_sort`
|
87
|
+
know which attributes of your model can be used to sort the collection.
|
88
|
+
Obviously these can't be virtual attributes because we're generating SQL here
|
89
|
+
(if you don't know what virtual attributes are then google is your friend).
|
90
|
+
|
91
|
+
As well as attributes in your model, you can also supply `belongs_to`
|
92
|
+
association names which will make `good_sort` sort your collection based on the
|
93
|
+
fields in a JOINed table.
|
94
|
+
|
95
|
+
class Author < ActiveRecord::Base
|
96
|
+
belongs_to :state
|
97
|
+
sort_on :name, :updated_at, :state
|
98
|
+
end
|
99
|
+
|
100
|
+
The convention is that this will use the `name` attribute of the associated
|
101
|
+
model, but if you want the sorting done using a different field then you can
|
102
|
+
just specify it using key => value style params, like so:
|
103
|
+
|
104
|
+
class Author < ActiveRecord::Base
|
105
|
+
belongs_to :state
|
106
|
+
sort_on :name, :updated_at, :state => :long_name
|
107
|
+
end
|
108
|
+
|
109
|
+
There's also no requirement to cram it all in on one line, you can have multiple
|
110
|
+
`sort_on` declarations, and they will just be accumulated.
|
111
|
+
|
112
|
+
### ActiveRecord::Base.sort\_by( params[:sort] )
|
113
|
+
|
114
|
+
This produces a `:order` hash suitable to be merged into your `Model.find` (or
|
115
|
+
`Model.paginate`) parameters based on the `:field` and `:down` input parameters.
|
116
|
+
|
117
|
+
### ActionView::Base#sort\_headers\_for( model\_name, header\_array, options = {} )
|
118
|
+
|
119
|
+
With no options, this will create `<th>` elements for each element of the
|
120
|
+
header_array, they will be given an id which, for the `name` field of our
|
121
|
+
`author` example would be `author_header_name`. If it has sorting set for it
|
122
|
+
with `sort_on` in your model, then it will also be wrapped in a gracefully
|
123
|
+
degrading re-sorting ajaxified link which will replace the element with id of
|
124
|
+
pluralized model name, so for our author example it will replace the element
|
125
|
+
with the id of `"authors"` (it will also show/hide an element with id of
|
126
|
+
`"spinner"` during the request). If the list is already sorted by that field,
|
127
|
+
then a class of either `"up"` or `"down"` will be added to the `<th>` element.
|
128
|
+
|
129
|
+
So, all of those things can be overridden. The options you can pass in are as
|
130
|
+
follows:
|
131
|
+
|
132
|
+
* **:spinner** - The id of the element to show/hide during the AJAX request, defaults to `:spinner`
|
133
|
+
* **:tag** - The type of element to wrap your header links in, defaults to `:th`
|
134
|
+
* **:header** - Options passed to the content_tag for the :tag wrapper.
|
135
|
+
* No defaults, but :id is set to `<model>\_header\_<field>` and :class will have `"up"` or `"down"` added to it appropriately.
|
136
|
+
* **:remote** - Options passed to `link\_to\_remote` as second arg, see the docs for `link\_to\_remote` for these options, defaults below:
|
137
|
+
* **:update** - Defaults to the lower-case pluralized and underscored version of your model name - ie. model\_name.tablelize
|
138
|
+
* **:before** - Defaults to showing the `:spinner` element (whatever you set that to, or `"spinner"` if you don't set it).
|
139
|
+
* **:complete** - Defaults to hiding the `:spinner` element
|
140
|
+
* **:method** - :get - you probably shouldn't change this
|
141
|
+
* **:url** - No point in setting this, it is overridden with the link URL.
|
142
|
+
* **:html** - Options pass to the `link\_to\_remote` as the third argument, see the docs for `link\_to\_remote` for these, defaults below:
|
143
|
+
* **:title** - Defaults to "Sort by #{sort\_field\_tag}". If you embed the sort\_field\_tag attribute in your string then that will be replaced with the field\_name.titlize for you, eg: :title => "Order by #{sort\_field\_tag}" If you want anything fancier then you can override `sort\_header\_title` and do whatever you want.
|
144
|
+
* **:html** - No point in setting this, it is overridden with the link URL.
|
data/VERSION.yml
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
module GoodSort
|
2
|
+
module Sorter
|
3
|
+
def sort_fields; @@sort_fields ||= {}; end
|
4
|
+
def sort_by(p)
|
5
|
+
return unless p && p[:field] && p[:down]
|
6
|
+
f = p[:field]
|
7
|
+
unless options = sort_fields[f.to_sym]
|
8
|
+
raise ArgumentError, "Requested field #{f} was not defined in #{class_name} for sorting"
|
9
|
+
end
|
10
|
+
options = options.dup
|
11
|
+
options[:order] += ' DESC' unless p[:down].blank?
|
12
|
+
options
|
13
|
+
end
|
14
|
+
protected
|
15
|
+
def sort_on(*fields)
|
16
|
+
fields.each do |f|
|
17
|
+
|
18
|
+
if f.is_a? String or f.is_a? Symbol
|
19
|
+
if self.columns_hash[f.to_s]
|
20
|
+
sort_fields[f.to_sym] = { :order => f.to_s }
|
21
|
+
next
|
22
|
+
else
|
23
|
+
# if it's not one of ours, we'll see if it's an association
|
24
|
+
f = { f => :name }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
if f.is_a? Hash
|
29
|
+
f.each do |k,v|
|
30
|
+
ass = association_for( k )
|
31
|
+
unless ass_has_attr( ass, v )
|
32
|
+
raise ArgumentError, "belongs_to association #{k} does not have specified column #{v}"
|
33
|
+
end
|
34
|
+
sort_fields[k.to_sym] = { :order => ass_to_table(ass) + '.' + v.to_s, :joins => k.to_sym }
|
35
|
+
end
|
36
|
+
next
|
37
|
+
end
|
38
|
+
raise ArgumentError, "Unrecognized option to sort_by"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
private
|
42
|
+
def association_for(k)
|
43
|
+
ass = self.reflect_on_association(k.to_sym) and ass.belongs_to? or raise ArgumentError, "belongs_to association not found for #{k}"
|
44
|
+
ass
|
45
|
+
end
|
46
|
+
def ass_has_attr(ass,v)
|
47
|
+
ass.klass.column_names.find{|e|e==v.to_s}
|
48
|
+
end
|
49
|
+
def ass_to_table(ass)
|
50
|
+
ass.class_name.tableize
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module GoodSort
|
2
|
+
module ViewHelpers
|
3
|
+
@@field_tag = '__FIELD__'
|
4
|
+
def sort_field_tag; @@field_tag; end
|
5
|
+
def sort_headers_for( m, h, options = {} )
|
6
|
+
|
7
|
+
id = m.to_s.singularize
|
8
|
+
m = m.to_s.classify
|
9
|
+
c = m.constantize
|
10
|
+
|
11
|
+
options.symbolize_keys!
|
12
|
+
options[:spinner] ||= :spinner
|
13
|
+
options[:tag] ||= :th
|
14
|
+
|
15
|
+
options[:header] ||= {}
|
16
|
+
options[:header].symbolize_keys!
|
17
|
+
|
18
|
+
options[:remote] ||= {}
|
19
|
+
options[:remote].symbolize_keys!
|
20
|
+
options[:remote][:update] ||= m.tableize
|
21
|
+
options[:remote][:before] = "$('#{options[:spinner]}').show()" unless options[:remote].has_key? :before
|
22
|
+
options[:remote][:complete] = "$('#{options[:spinner]}').hide()" unless options[:remote].has_key? :complete
|
23
|
+
options[:remote][:method] ||= :get
|
24
|
+
|
25
|
+
# save these for pagination calls later in the request
|
26
|
+
@remote_options = options[:remote].dup
|
27
|
+
|
28
|
+
options[:html] ||= {}
|
29
|
+
options[:html].symbolize_keys!
|
30
|
+
options[:html][:title] ||= "Sort by #{sort_field_tag}"
|
31
|
+
|
32
|
+
sf = c.sort_fields
|
33
|
+
logger.warn "GoodSort: #{m} has not had any sort_on fields set" if sf.nil?
|
34
|
+
|
35
|
+
h.each do |f|
|
36
|
+
options[:header][:id] = "#{id}_header_#{f}"
|
37
|
+
|
38
|
+
text = yield(f) if block_given?
|
39
|
+
text ||= f.to_s.titleize
|
40
|
+
|
41
|
+
unless sf[f.to_sym]
|
42
|
+
concat content_tag(options[:tag], text, options[:header])
|
43
|
+
next
|
44
|
+
end
|
45
|
+
|
46
|
+
concat sort_header( f, text, options )
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def sort_header(f, text, options )
|
51
|
+
params[:sort] ||= {}
|
52
|
+
|
53
|
+
tag_options = options[:header].dup
|
54
|
+
if params[:sort][:field] == f.to_s
|
55
|
+
tag_options[:class] ||= ''
|
56
|
+
(tag_options[:class] += params[:sort][:down].blank? ? ' up' : ' down' ).strip!
|
57
|
+
end
|
58
|
+
content_tag( options[:tag], sort_link(f, text, options), tag_options )
|
59
|
+
end
|
60
|
+
|
61
|
+
def sort_link(f, text, options)
|
62
|
+
s = { :field => f, :down => params[:sort][:field] == f.to_s && params[:sort][:down].blank? ? true : nil }
|
63
|
+
ps = params.merge( :sort => s, :page => nil )
|
64
|
+
|
65
|
+
options[:remote][:url] = options[:html][:href] = url_for( :params => ps )
|
66
|
+
title = sort_header_title( text, options)
|
67
|
+
link_to_remote(text, options[:remote], options[:html].merge( :title => title))
|
68
|
+
end
|
69
|
+
|
70
|
+
def sort_header_title( field, options )
|
71
|
+
options[:html][:title].gsub(sort_field_tag, field)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module GoodSort
|
2
|
+
module WillPaginate
|
3
|
+
def self.included(base)
|
4
|
+
base.send :include, InstanceMethods
|
5
|
+
base.alias_method_chain :will_paginate, :good_sort
|
6
|
+
end
|
7
|
+
|
8
|
+
module InstanceMethods
|
9
|
+
def will_paginate_with_good_sort( collection = nil, options = {} )
|
10
|
+
will_paginate_without_good_sort( collection, options.merge( :remote => @remote_options, :params => params ) )
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/lib/good_sort.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
module GoodSort
|
2
|
+
class << self
|
3
|
+
def shwing
|
4
|
+
require 'good_sort/view_helpers'
|
5
|
+
ActionView::Base.send :include, ViewHelpers
|
6
|
+
|
7
|
+
require 'good_sort/sorter'
|
8
|
+
ActiveRecord::Base.send :extend, Sorter
|
9
|
+
|
10
|
+
if ActionView::Base.instance_methods.include? 'will_paginate'
|
11
|
+
require 'good_sort/will_paginate'
|
12
|
+
ActionView::Base.send :include, WillPaginate
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
GoodSort.shwing
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'good_sort/sorter'
|
3
|
+
require 'active_support'
|
4
|
+
|
5
|
+
class GoodSortSorterTest < Test::Unit::TestCase
|
6
|
+
include GoodSort::Sorter
|
7
|
+
def columns_hash; { 'foo' => true, 'bar' => true }; end
|
8
|
+
def class_name; "foobar"; end
|
9
|
+
|
10
|
+
def reflect_on_association(a)
|
11
|
+
ass = mock()
|
12
|
+
ass.stubs(:belongs_to?).returns(a.to_sym == :ass_exist)
|
13
|
+
|
14
|
+
ass.stubs(:class_name).returns('ass_exists')
|
15
|
+
|
16
|
+
ass_klass = mock()
|
17
|
+
ass_klass.stubs(:column_names).returns( %w{name last_name} )
|
18
|
+
|
19
|
+
ass.stubs(:klass).returns(ass_klass)
|
20
|
+
|
21
|
+
ass
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_sort_on_our_attributes
|
25
|
+
sort_on :foo, :bar
|
26
|
+
assert_equal 2, sort_fields.length
|
27
|
+
assert_equal( { :order => 'foo' }, sort_fields[:foo])
|
28
|
+
assert_equal( { :order => 'bar' }, sort_fields[:bar])
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_association_sort_fields
|
32
|
+
sort_on :ass_exist => :last_name
|
33
|
+
assert_equal 'ass_exists.last_name', sort_fields[:ass_exist][:order]
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_default_association_sort_field
|
37
|
+
sort_on :ass_exist
|
38
|
+
assert_equal 'ass_exists.name', sort_fields[:ass_exist][:order]
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_argument_errors
|
42
|
+
assert_raise ArgumentError do
|
43
|
+
sort_on :ass_imaginary
|
44
|
+
end
|
45
|
+
|
46
|
+
assert_raise ArgumentError do
|
47
|
+
sort_on false
|
48
|
+
end
|
49
|
+
|
50
|
+
assert_raise ArgumentError do
|
51
|
+
sort_on :ass_exist => :nonexistent
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_multiple_declarations
|
56
|
+
sort_on :foo
|
57
|
+
sort_on :bar
|
58
|
+
assert_equal 2, sort_fields.length
|
59
|
+
assert_equal( { :order => 'foo' }, sort_fields[:foo])
|
60
|
+
assert_equal( { :order => 'bar' }, sort_fields[:bar])
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_sort_by
|
64
|
+
sort_on :foo
|
65
|
+
assert_equal( { :order => 'foo' }, sort_by( :field => 'foo', :down => '' ))
|
66
|
+
assert_equal( { :order => 'foo DESC'}, sort_by( :field => 'foo', :down => 'true' ))
|
67
|
+
assert_raise ArgumentError do
|
68
|
+
sort_by( :field => 'bar', :down => '' )
|
69
|
+
end
|
70
|
+
assert_nil sort_by nil
|
71
|
+
assert_nil sort_by( :field => 'foo' )
|
72
|
+
assert_nil sort_by( :down => '' )
|
73
|
+
assert_nil sort_by( :down => 'true' )
|
74
|
+
|
75
|
+
sort_on :ass_exist
|
76
|
+
assert_equal( { :joins => :ass_exist, :order => 'ass_exists.name' }, sort_by( :field => 'ass_exist', :down => '' ))
|
77
|
+
assert_equal( { :joins => :ass_exist, :order => 'ass_exists.name DESC' }, sort_by( :field => 'ass_exist', :down => 'true' ))
|
78
|
+
end
|
79
|
+
|
80
|
+
def teardown
|
81
|
+
GoodSort::Sorter.send :class_variable_set, :@@sort_fields, {}
|
82
|
+
end
|
83
|
+
end
|
data/test/test_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: JasonKing-good_sort
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jason King
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-04-01 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description:
|
17
|
+
email: jk@silentcow.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README.markdown
|
24
|
+
- LICENSE
|
25
|
+
files:
|
26
|
+
- README.markdown
|
27
|
+
- VERSION.yml
|
28
|
+
- lib/good_sort
|
29
|
+
- lib/good_sort/sorter.rb
|
30
|
+
- lib/good_sort/view_helpers.rb
|
31
|
+
- lib/good_sort/will_paginate.rb
|
32
|
+
- lib/good_sort.rb
|
33
|
+
- test/good_sort_test.rb
|
34
|
+
- test/test_helper.rb
|
35
|
+
- LICENSE
|
36
|
+
has_rdoc: true
|
37
|
+
homepage: http://github.com/JasonKing/good_sort
|
38
|
+
post_install_message:
|
39
|
+
rdoc_options:
|
40
|
+
- --inline-source
|
41
|
+
- --charset=UTF-8
|
42
|
+
require_paths:
|
43
|
+
- lib
|
44
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: "0"
|
49
|
+
version:
|
50
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: "0"
|
55
|
+
version:
|
56
|
+
requirements: []
|
57
|
+
|
58
|
+
rubyforge_project:
|
59
|
+
rubygems_version: 1.2.0
|
60
|
+
signing_key:
|
61
|
+
specification_version: 2
|
62
|
+
summary: TODO
|
63
|
+
test_files: []
|
64
|
+
|