@@ -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 |
# - |
|
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,88 +46,133 | |||||
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' |
|
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 |
# |
|
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 |
# - |
|
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 |
|
|
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 = |
|
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 | # 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 | |||
123 | # Add project_id to url_options |
|
169 | # Add project_id to url_options | |
124 | url_options = url_options.merge(:project_id => params[:project_id]) if params.has_key?(:project_id) |
|
170 | url_options = url_options.merge(:project_id => params[:project_id]) if params.has_key?(:project_id) | |
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) || |
|
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 | ' ' * n |
|
|||
164 | end |
|
|||
165 |
|
||||
166 | # Return capitalized title. |
|
|||
167 | def titleize(title) |
|
|||
168 | title.split.map {|w| w.capitalize }.join(' ') |
|
|||
169 | end |
|
|||
170 |
|
||||
171 | end |
|
204 | end | |
|
205 |
@@ -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