##// END OF EJS Templates
SortHelper refactoring:...
Jean-Philippe Lang -
r2503:2b585407cb66
parent child
Show More
@@ -0,0 +1,73
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 require File.dirname(__FILE__) + '/../../test_helper'
19
20 class SortHelperTest < HelperTestCase
21 include SortHelper
22
23 def test_default_sort_clause_with_array
24 sort_init 'attr1', 'desc'
25 sort_update(['attr1', 'attr2'])
26
27 assert_equal 'attr1 DESC', sort_clause
28 end
29
30 def test_default_sort_clause_with_hash
31 sort_init 'attr1', 'desc'
32 sort_update({'attr1' => 'table1.attr1', 'attr2' => 'table2.attr2'})
33
34 assert_equal 'table1.attr1 DESC', sort_clause
35 end
36
37 def test_params_sort
38 @sort_param = 'attr1,attr2:desc'
39
40 sort_init 'attr1', 'desc'
41 sort_update({'attr1' => 'table1.attr1', 'attr2' => 'table2.attr2'})
42
43 assert_equal 'table1.attr1, table2.attr2 DESC', sort_clause
44 assert_equal 'attr1,attr2:desc', @session['foo_bar_sort']
45 end
46
47 def test_invalid_params_sort
48 @sort_param = 'attr3'
49
50 sort_init 'attr1', 'desc'
51 sort_update({'attr1' => 'table1.attr1', 'attr2' => 'table2.attr2'})
52
53 assert_nil sort_clause
54 assert_equal '', @session['foo_bar_sort']
55 end
56
57 def test_invalid_order_params_sort
58 @sort_param = 'attr1:foo:bar,attr2'
59
60 sort_init 'attr1', 'desc'
61 sort_update({'attr1' => 'table1.attr1', 'attr2' => 'table2.attr2'})
62
63 assert_equal 'table1.attr1, table2.attr2', sort_clause
64 assert_equal 'attr1,attr2', @session['foo_bar_sort']
65 end
66
67 private
68
69 def controller_name; 'foo'; end
70 def action_name; 'bar'; end
71 def params; {:sort => @sort_param}; end
72 def session; @session ||= {}; end
73 end
@@ -1,11 +1,12
1 # Helpers to sort tables using clickable column headers.
1 # Helpers to sort tables using clickable column headers.
2 #
2 #
3 # Author: Stuart Rackham <srackham@methods.co.nz>, March 2005.
3 # Author: Stuart Rackham <srackham@methods.co.nz>, March 2005.
4 # Jean-Philippe Lang, 2009
4 # License: This source code is released under the MIT license.
5 # License: This source code is released under the MIT license.
5 #
6 #
6 # - Consecutive clicks toggle the column's sort order.
7 # - Consecutive clicks toggle the column's sort order.
7 # - Sort state is maintained by a session hash entry.
8 # - Sort state is maintained by a session hash entry.
8 # - Icon image identifies sort column and state.
9 # - CSS classes identify sort column and state.
9 # - Typically used in conjunction with the Pagination module.
10 # - Typically used in conjunction with the Pagination module.
10 #
11 #
11 # Example code snippets:
12 # Example code snippets:
@@ -17,7 +18,7
17 #
18 #
18 # def list
19 # def list
19 # sort_init 'last_name'
20 # sort_init 'last_name'
20 # sort_update
21 # sort_update %w(first_name, last_name)
21 # @items = Contact.find_all nil, sort_clause
22 # @items = Contact.find_all nil, sort_clause
22 # end
23 # end
23 #
24 #
@@ -28,7 +29,7
28 #
29 #
29 # def list
30 # def list
30 # sort_init 'last_name'
31 # sort_init 'last_name'
31 # sort_update
32 # sort_update %w(first_name, last_name)
32 # @contact_pages, @items = paginate :contacts,
33 # @contact_pages, @items = paginate :contacts,
33 # :order_by => sort_clause,
34 # :order_by => sort_clause,
34 # :per_page => 10
35 # :per_page => 10
@@ -45,78 +46,123
45 # </tr>
46 # </tr>
46 # </thead>
47 # </thead>
47 #
48 #
48 # - The ascending and descending sort icon images are sort_asc.png and
49 # - Introduces instance variables: @sort_default, @sort_criteria
49 # sort_desc.png and reside in the application's images directory.
50 # - Introduces param :sort
50 # - Introduces instance variables: @sort_name, @sort_default.
51 # - Introduces params :sort_key and :sort_order.
52 #
51 #
52
53 module SortHelper
53 module SortHelper
54 class SortCriteria
55
56 def initialize
57 @criteria = []
58 end
59
60 def available_criteria=(criteria)
61 unless criteria.is_a?(Hash)
62 criteria = criteria.inject({}) {|h,k| h[k] = k; h}
63 end
64 @available_criteria = criteria
65 end
66
67 def from_param(param)
68 @criteria = param.to_s.split(',').collect {|s| s.split(':')[0..1]}
69 normalize!
70 end
71
72 def to_param
73 @criteria.collect {|k,o| k + (o ? '' : ':desc')}.join(',')
74 end
75
76 def to_sql
77 sql = @criteria.collect do |k,o|
78 if s = @available_criteria[k]
79 (o ? s.to_a : s.to_a.collect {|c| "#{c} DESC"}).join(', ')
80 end
81 end.compact.join(', ')
82 sql.blank? ? nil : sql
83 end
84
85 def add!(key, asc)
86 @criteria.delete_if {|k,o| k == key}
87 @criteria = [[key, asc]] + @criteria
88 normalize!
89 end
90
91 def add(*args)
92 r = self.class.new.from_param(to_param)
93 r.add!(*args)
94 r
95 end
96
97 def first_key
98 @criteria.first && @criteria.first.first
99 end
100
101 def first_asc?
102 @criteria.first && @criteria.first.last
103 end
104
105 private
106
107 def normalize!
108 @criteria = @criteria.collect {|s| [s.first, (s.last == false || s.last == 'desc') ? false : true]}
109 @criteria = @criteria.select {|k,o| @available_criteria.has_key?(k)} if @available_criteria
110 @criteria.slice!(3)
111 self
112 end
113 end
54
114
55 # Initializes the default sort column (default_key) and sort order
115 # Initializes the default sort column (default_key) and sort order
56 # (default_order).
116 # (default_order).
57 #
117 #
58 # - default_key is a column attribute name.
118 # - default_key is a column attribute name.
59 # - default_order is 'asc' or 'desc'.
119 # - default_order is 'asc' or 'desc'.
60 # - name is the name of the session hash entry that stores the sort state,
61 # defaults to '<controller_name>_sort'.
62 #
120 #
63 def sort_init(default_key, default_order='asc', name=nil)
121 def sort_init(default_key, default_order='asc')
64 @sort_name = name || params[:controller] + params[:action] + '_sort'
122 @sort_default = "#{default_key}:#{default_order}"
65 @sort_default = {:key => default_key, :order => default_order}
66 end
123 end
67
124
68 # Updates the sort state. Call this in the controller prior to calling
125 # Updates the sort state. Call this in the controller prior to calling
69 # sort_clause.
126 # sort_clause.
70 # sort_keys can be either an array or a hash of allowed keys
127 # - criteria can be either an array or a hash of allowed keys
71 def sort_update(sort_keys)
128 #
72 sort_key = params[:sort_key]
129 def sort_update(criteria)
73 sort_key = nil unless (sort_keys.is_a?(Array) ? sort_keys.include?(sort_key) : sort_keys[sort_key])
130 sort_name = controller_name + '_' + action_name + '_sort'
74
75 sort_order = (params[:sort_order] == 'desc' ? 'DESC' : 'ASC')
76
77 if sort_key
78 sort = {:key => sort_key, :order => sort_order}
79 elsif session[@sort_name]
80 sort = session[@sort_name] # Previous sort.
81 else
82 sort = @sort_default
83 end
84 session[@sort_name] = sort
85
131
86 sort_column = (sort_keys.is_a?(Hash) ? sort_keys[sort[:key]] : sort[:key])
132 @sort_criteria = SortCriteria.new
87 @sort_clause = (sort_column.blank? ? nil : [sort_column].flatten.collect {|s| "#{s} #{sort[:order]}"}.join(','))
133 @sort_criteria.available_criteria = criteria
134 @sort_criteria.from_param(params[:sort] || session[sort_name] || @sort_default)
135 session[sort_name] = @sort_criteria.to_param
88 end
136 end
89
137
90 # Returns an SQL sort clause corresponding to the current sort state.
138 # Returns an SQL sort clause corresponding to the current sort state.
91 # Use this to sort the controller's table items collection.
139 # Use this to sort the controller's table items collection.
92 #
140 #
93 def sort_clause()
141 def sort_clause()
94 @sort_clause
142 @sort_criteria.to_sql
95 end
143 end
96
144
97 # Returns a link which sorts by the named column.
145 # Returns a link which sorts by the named column.
98 #
146 #
99 # - column is the name of an attribute in the sorted record collection.
147 # - column is the name of an attribute in the sorted record collection.
100 # - The optional caption explicitly specifies the displayed link text.
148 # - the optional caption explicitly specifies the displayed link text.
101 # - A sort icon image is positioned to the right of the sort link.
149 # - 2 CSS classes reflect the state of the link: sort and asc or desc
102 #
150 #
103 def sort_link(column, caption, default_order)
151 def sort_link(column, caption, default_order)
104 key, order = session[@sort_name][:key], session[@sort_name][:order]
152 css, order = nil, default_order
105 if key == column
153
106 if order.downcase == 'asc'
154 if column.to_s == @sort_criteria.first_key
107 icon = 'sort_asc.png'
155 if @sort_criteria.first_asc?
156 css = 'sort asc'
108 order = 'desc'
157 order = 'desc'
109 else
158 else
110 icon = 'sort_desc.png'
159 css = 'sort desc'
111 order = 'asc'
160 order = 'asc'
112 end
161 end
113 else
114 icon = nil
115 order = default_order
116 end
162 end
117 caption = titleize(Inflector::humanize(column)) unless caption
163 caption = column.to_s.humanize unless caption
118
164
119 sort_options = { :sort_key => column, :sort_order => order }
165 sort_options = { :sort => @sort_criteria.add(column.to_s, order).to_param }
120 # don't reuse params if filters are present
166 # don't reuse params if filters are present
121 url_options = params.has_key?(:set_filter) ? sort_options : params.merge(sort_options)
167 url_options = params.has_key?(:set_filter) ? sort_options : params.merge(sort_options)
122
168
@@ -125,8 +171,8 module SortHelper
125
171
126 link_to_remote(caption,
172 link_to_remote(caption,
127 {:update => "content", :url => url_options, :method => :get},
173 {:update => "content", :url => url_options, :method => :get},
128 {:href => url_for(url_options)}) +
174 {:href => url_for(url_options),
129 (icon ? nbsp(2) + image_tag(icon) : '')
175 :class => css})
130 end
176 end
131
177
132 # Returns a table header <th> tag with a sort link for the named column
178 # Returns a table header <th> tag with a sort link for the named column
@@ -150,22 +196,10 module SortHelper
150 # </th>
196 # </th>
151 #
197 #
152 def sort_header_tag(column, options = {})
198 def sort_header_tag(column, options = {})
153 caption = options.delete(:caption) || titleize(Inflector::humanize(column))
199 caption = options.delete(:caption) || column.to_s.humanize
154 default_order = options.delete(:default_order) || 'asc'
200 default_order = options.delete(:default_order) || 'asc'
155 options[:title]= l(:label_sort_by, "\"#{caption}\"") unless options[:title]
201 options[:title] = l(:label_sort_by, "\"#{caption}\"") unless options[:title]
156 content_tag('th', sort_link(column, caption, default_order), options)
202 content_tag('th', sort_link(column, caption, default_order), options)
157 end
203 end
158
159 private
160
161 # Return n non-breaking spaces.
162 def nbsp(n)
163 '&nbsp;' * n
164 end
165
166 # Return capitalized title.
167 def titleize(title)
168 title.split.map {|w| w.capitalize }.join(' ')
169 end
204 end
170
205
171 end
@@ -142,6 +142,10 table p {margin:0;}
142 .odd {background-color:#f6f7f8;}
142 .odd {background-color:#f6f7f8;}
143 .even {background-color: #fff;}
143 .even {background-color: #fff;}
144
144
145 a.sort { padding-right: 16px; background-position: 100% 50%; background-repeat: no-repeat; }
146 a.sort.asc { background-image: url(../images/sort_asc.png); }
147 a.sort.desc { background-image: url(../images/sort_desc.png); }
148
145 .highlight { background-color: #FCFD8D;}
149 .highlight { background-color: #FCFD8D;}
146 .highlight.token-1 { background-color: #faa;}
150 .highlight.token-1 { background-color: #faa;}
147 .highlight.token-2 { background-color: #afa;}
151 .highlight.token-2 { background-color: #afa;}
General Comments 0
You need to be logged in to leave comments. Login now