@@ -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 | 1 | # Helpers to sort tables using clickable column headers. |
|
2 | 2 | # |
|
3 | 3 | # Author: Stuart Rackham <srackham@methods.co.nz>, March 2005. |
|
4 | # Jean-Philippe Lang, 2009 | |
|
4 | 5 | # License: This source code is released under the MIT license. |
|
5 | 6 | # |
|
6 | 7 | # - Consecutive clicks toggle the column's sort order. |
|
7 | 8 | # - Sort state is maintained by a session hash entry. |
|
8 |
# - |
|
|
9 | # - CSS classes identify sort column and state. | |
|
9 | 10 | # - Typically used in conjunction with the Pagination module. |
|
10 | 11 | # |
|
11 | 12 | # Example code snippets: |
@@ -17,7 +18,7 | |||
|
17 | 18 | # |
|
18 | 19 | # def list |
|
19 | 20 | # sort_init 'last_name' |
|
20 | # sort_update | |
|
21 | # sort_update %w(first_name, last_name) | |
|
21 | 22 | # @items = Contact.find_all nil, sort_clause |
|
22 | 23 | # end |
|
23 | 24 | # |
@@ -28,7 +29,7 | |||
|
28 | 29 | # |
|
29 | 30 | # def list |
|
30 | 31 | # sort_init 'last_name' |
|
31 | # sort_update | |
|
32 | # sort_update %w(first_name, last_name) | |
|
32 | 33 | # @contact_pages, @items = paginate :contacts, |
|
33 | 34 | # :order_by => sort_clause, |
|
34 | 35 | # :per_page => 10 |
@@ -45,78 +46,123 | |||
|
45 | 46 | # </tr> |
|
46 | 47 | # </thead> |
|
47 | 48 | # |
|
48 | # - The ascending and descending sort icon images are sort_asc.png and | |
|
49 | # sort_desc.png and reside in the application's images directory. | |
|
50 | # - Introduces instance variables: @sort_name, @sort_default. | |
|
51 | # - Introduces params :sort_key and :sort_order. | |
|
49 | # - Introduces instance variables: @sort_default, @sort_criteria | |
|
50 | # - Introduces param :sort | |
|
52 | 51 | # |
|
52 | ||
|
53 | 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 | 115 | # Initializes the default sort column (default_key) and sort order |
|
56 | 116 | # (default_order). |
|
57 | 117 | # |
|
58 | 118 | # - default_key is a column attribute name. |
|
59 | 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' |
|
|
64 | @sort_name = name || params[:controller] + params[:action] + '_sort' | |
|
65 | @sort_default = {:key => default_key, :order => default_order} | |
|
121 | def sort_init(default_key, default_order='asc') | |
|
122 | @sort_default = "#{default_key}:#{default_order}" | |
|
66 | 123 | end |
|
67 | 124 | |
|
68 | 125 | # Updates the sort state. Call this in the controller prior to calling |
|
69 | 126 | # sort_clause. |
|
70 |
# |
|
|
71 | def sort_update(sort_keys) | |
|
72 | sort_key = params[:sort_key] | |
|
73 | sort_key = nil unless (sort_keys.is_a?(Array) ? sort_keys.include?(sort_key) : sort_keys[sort_key]) | |
|
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 | |
|
127 | # - criteria can be either an array or a hash of allowed keys | |
|
128 | # | |
|
129 | def sort_update(criteria) | |
|
130 | sort_name = controller_name + '_' + action_name + '_sort' | |
|
85 | 131 | |
|
86 | sort_column = (sort_keys.is_a?(Hash) ? sort_keys[sort[:key]] : sort[:key]) | |
|
87 | @sort_clause = (sort_column.blank? ? nil : [sort_column].flatten.collect {|s| "#{s} #{sort[:order]}"}.join(',')) | |
|
132 | @sort_criteria = SortCriteria.new | |
|
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 | 136 | end |
|
89 | 137 | |
|
90 | 138 | # Returns an SQL sort clause corresponding to the current sort state. |
|
91 | 139 | # Use this to sort the controller's table items collection. |
|
92 | 140 | # |
|
93 | 141 | def sort_clause() |
|
94 | @sort_clause | |
|
142 | @sort_criteria.to_sql | |
|
95 | 143 | end |
|
96 | 144 | |
|
97 | 145 | # Returns a link which sorts by the named column. |
|
98 | 146 | # |
|
99 | 147 | # - column is the name of an attribute in the sorted record collection. |
|
100 |
# - |
|
|
101 | # - A sort icon image is positioned to the right of the sort link. | |
|
148 | # - the optional caption explicitly specifies the displayed link text. | |
|
149 | # - 2 CSS classes reflect the state of the link: sort and asc or desc | |
|
102 | 150 | # |
|
103 | 151 | def sort_link(column, caption, default_order) |
|
104 | key, order = session[@sort_name][:key], session[@sort_name][:order] | |
|
105 | if key == column | |
|
106 | if order.downcase == 'asc' | |
|
107 | icon = 'sort_asc.png' | |
|
152 | css, order = nil, default_order | |
|
153 | ||
|
154 | if column.to_s == @sort_criteria.first_key | |
|
155 | if @sort_criteria.first_asc? | |
|
156 | css = 'sort asc' | |
|
108 | 157 | order = 'desc' |
|
109 | 158 | else |
|
110 |
|
|
|
159 | css = 'sort desc' | |
|
111 | 160 | order = 'asc' |
|
112 | 161 | end |
|
113 | else | |
|
114 | icon = nil | |
|
115 | order = default_order | |
|
116 | 162 | end |
|
117 |
caption = |
|
|
163 | caption = column.to_s.humanize unless caption | |
|
118 | 164 | |
|
119 |
sort_options = { :sort |
|
|
165 | sort_options = { :sort => @sort_criteria.add(column.to_s, order).to_param } | |
|
120 | 166 | # don't reuse params if filters are present |
|
121 | 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 | 172 | link_to_remote(caption, |
|
127 | 173 | {:update => "content", :url => url_options, :method => :get}, |
|
128 |
{:href => url_for(url_options) |
|
|
129 | (icon ? nbsp(2) + image_tag(icon) : '') | |
|
174 | {:href => url_for(url_options), | |
|
175 | :class => css}) | |
|
130 | 176 | end |
|
131 | 177 | |
|
132 | 178 | # Returns a table header <th> tag with a sort link for the named column |
@@ -150,22 +196,10 module SortHelper | |||
|
150 | 196 | # </th> |
|
151 | 197 | # |
|
152 | 198 | def sort_header_tag(column, options = {}) |
|
153 |
caption = options.delete(:caption) || |
|
|
199 | caption = options.delete(:caption) || column.to_s.humanize | |
|
154 | 200 | default_order = options.delete(:default_order) || 'asc' |
|
155 | 201 | options[:title]= l(:label_sort_by, "\"#{caption}\"") unless options[:title] |
|
156 | 202 | content_tag('th', sort_link(column, caption, default_order), options) |
|
157 | 203 | end |
|
158 | ||
|
159 | private | |
|
160 | ||
|
161 | # Return n non-breaking spaces. | |
|
162 | def nbsp(n) | |
|
163 | ' ' * n | |
|
164 | end | |
|
165 | ||
|
166 | # Return capitalized title. | |
|
167 | def titleize(title) | |
|
168 | title.split.map {|w| w.capitalize }.join(' ') | |
|
169 | 204 |
|
|
170 | 205 | |
|
171 | end |
@@ -142,6 +142,10 table p {margin:0;} | |||
|
142 | 142 | .odd {background-color:#f6f7f8;} |
|
143 | 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 | 149 | .highlight { background-color: #FCFD8D;} |
|
146 | 150 | .highlight.token-1 { background-color: #faa;} |
|
147 | 151 | .highlight.token-2 { background-color: #afa;} |
General Comments 0
You need to be logged in to leave comments.
Login now