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 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,4 @@
1
+ ---
2
+ :patch: 1
3
+ :major: 0
4
+ :minor: 1
@@ -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
@@ -0,0 +1,7 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'redgreen'
4
+ require 'mocha'
5
+
6
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
7
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
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
+