##// END OF EJS Templates
Previous/Next navigation on a issue is not displayed on r15639 and later (#23781)....
Jean-Philippe Lang -
r15468:7daa5b750fe6
parent child
Show More
@@ -1,345 +1,348
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 module QueriesHelper
20 module QueriesHelper
21 include ApplicationHelper
21 include ApplicationHelper
22
22
23 def filters_options_for_select(query)
23 def filters_options_for_select(query)
24 ungrouped = []
24 ungrouped = []
25 grouped = {}
25 grouped = {}
26 query.available_filters.map do |field, field_options|
26 query.available_filters.map do |field, field_options|
27 if field_options[:type] == :relation
27 if field_options[:type] == :relation
28 group = :label_relations
28 group = :label_relations
29 elsif field_options[:type] == :tree
29 elsif field_options[:type] == :tree
30 group = query.is_a?(IssueQuery) ? :label_relations : nil
30 group = query.is_a?(IssueQuery) ? :label_relations : nil
31 elsif field =~ /^(.+)\./
31 elsif field =~ /^(.+)\./
32 # association filters
32 # association filters
33 group = "field_#{$1}"
33 group = "field_#{$1}"
34 elsif %w(member_of_group assigned_to_role).include?(field)
34 elsif %w(member_of_group assigned_to_role).include?(field)
35 group = :field_assigned_to
35 group = :field_assigned_to
36 elsif field_options[:type] == :date_past || field_options[:type] == :date
36 elsif field_options[:type] == :date_past || field_options[:type] == :date
37 group = :label_date
37 group = :label_date
38 end
38 end
39 if group
39 if group
40 (grouped[group] ||= []) << [field_options[:name], field]
40 (grouped[group] ||= []) << [field_options[:name], field]
41 else
41 else
42 ungrouped << [field_options[:name], field]
42 ungrouped << [field_options[:name], field]
43 end
43 end
44 end
44 end
45 # Don't group dates if there's only one (eg. time entries filters)
45 # Don't group dates if there's only one (eg. time entries filters)
46 if grouped[:label_date].try(:size) == 1
46 if grouped[:label_date].try(:size) == 1
47 ungrouped << grouped.delete(:label_date).first
47 ungrouped << grouped.delete(:label_date).first
48 end
48 end
49 s = options_for_select([[]] + ungrouped)
49 s = options_for_select([[]] + ungrouped)
50 if grouped.present?
50 if grouped.present?
51 localized_grouped = grouped.map {|k,v| [l(k), v]}
51 localized_grouped = grouped.map {|k,v| [l(k), v]}
52 s << grouped_options_for_select(localized_grouped)
52 s << grouped_options_for_select(localized_grouped)
53 end
53 end
54 s
54 s
55 end
55 end
56
56
57 def query_filters_hidden_tags(query)
57 def query_filters_hidden_tags(query)
58 tags = ''.html_safe
58 tags = ''.html_safe
59 query.filters.each do |field, options|
59 query.filters.each do |field, options|
60 tags << hidden_field_tag("f[]", field, :id => nil)
60 tags << hidden_field_tag("f[]", field, :id => nil)
61 tags << hidden_field_tag("op[#{field}]", options[:operator], :id => nil)
61 tags << hidden_field_tag("op[#{field}]", options[:operator], :id => nil)
62 options[:values].each do |value|
62 options[:values].each do |value|
63 tags << hidden_field_tag("v[#{field}][]", value, :id => nil)
63 tags << hidden_field_tag("v[#{field}][]", value, :id => nil)
64 end
64 end
65 end
65 end
66 tags
66 tags
67 end
67 end
68
68
69 def query_columns_hidden_tags(query)
69 def query_columns_hidden_tags(query)
70 tags = ''.html_safe
70 tags = ''.html_safe
71 query.columns.each do |column|
71 query.columns.each do |column|
72 tags << hidden_field_tag("c[]", column.name, :id => nil)
72 tags << hidden_field_tag("c[]", column.name, :id => nil)
73 end
73 end
74 tags
74 tags
75 end
75 end
76
76
77 def query_hidden_tags(query)
77 def query_hidden_tags(query)
78 query_filters_hidden_tags(query) + query_columns_hidden_tags(query)
78 query_filters_hidden_tags(query) + query_columns_hidden_tags(query)
79 end
79 end
80
80
81 def group_by_column_select_tag(query)
81 def group_by_column_select_tag(query)
82 options = [[]] + query.groupable_columns.collect {|c| [c.caption, c.name.to_s]}
82 options = [[]] + query.groupable_columns.collect {|c| [c.caption, c.name.to_s]}
83 select_tag('group_by', options_for_select(options, @query.group_by))
83 select_tag('group_by', options_for_select(options, @query.group_by))
84 end
84 end
85
85
86 def available_block_columns_tags(query)
86 def available_block_columns_tags(query)
87 tags = ''.html_safe
87 tags = ''.html_safe
88 query.available_block_columns.each do |column|
88 query.available_block_columns.each do |column|
89 tags << content_tag('label', check_box_tag('c[]', column.name.to_s, query.has_column?(column), :id => nil) + " #{column.caption}", :class => 'inline')
89 tags << content_tag('label', check_box_tag('c[]', column.name.to_s, query.has_column?(column), :id => nil) + " #{column.caption}", :class => 'inline')
90 end
90 end
91 tags
91 tags
92 end
92 end
93
93
94 def available_totalable_columns_tags(query)
94 def available_totalable_columns_tags(query)
95 tags = ''.html_safe
95 tags = ''.html_safe
96 query.available_totalable_columns.each do |column|
96 query.available_totalable_columns.each do |column|
97 tags << content_tag('label', check_box_tag('t[]', column.name.to_s, query.totalable_columns.include?(column), :id => nil) + " #{column.caption}", :class => 'inline')
97 tags << content_tag('label', check_box_tag('t[]', column.name.to_s, query.totalable_columns.include?(column), :id => nil) + " #{column.caption}", :class => 'inline')
98 end
98 end
99 tags << hidden_field_tag('t[]', '')
99 tags << hidden_field_tag('t[]', '')
100 tags
100 tags
101 end
101 end
102
102
103 def query_available_inline_columns_options(query)
103 def query_available_inline_columns_options(query)
104 (query.available_inline_columns - query.columns).reject(&:frozen?).collect {|column| [column.caption, column.name]}
104 (query.available_inline_columns - query.columns).reject(&:frozen?).collect {|column| [column.caption, column.name]}
105 end
105 end
106
106
107 def query_selected_inline_columns_options(query)
107 def query_selected_inline_columns_options(query)
108 (query.inline_columns & query.available_inline_columns).reject(&:frozen?).collect {|column| [column.caption, column.name]}
108 (query.inline_columns & query.available_inline_columns).reject(&:frozen?).collect {|column| [column.caption, column.name]}
109 end
109 end
110
110
111 def render_query_columns_selection(query, options={})
111 def render_query_columns_selection(query, options={})
112 tag_name = (options[:name] || 'c') + '[]'
112 tag_name = (options[:name] || 'c') + '[]'
113 render :partial => 'queries/columns', :locals => {:query => query, :tag_name => tag_name}
113 render :partial => 'queries/columns', :locals => {:query => query, :tag_name => tag_name}
114 end
114 end
115
115
116 def grouped_query_results(items, query, item_count_by_group, &block)
116 def grouped_query_results(items, query, item_count_by_group, &block)
117 previous_group, first = false, true
117 previous_group, first = false, true
118 totals_by_group = query.totalable_columns.inject({}) do |h, column|
118 totals_by_group = query.totalable_columns.inject({}) do |h, column|
119 h[column] = query.total_by_group_for(column)
119 h[column] = query.total_by_group_for(column)
120 h
120 h
121 end
121 end
122 items.each do |item|
122 items.each do |item|
123 group_name = group_count = nil
123 group_name = group_count = nil
124 if query.grouped?
124 if query.grouped?
125 group = query.group_by_column.value(item)
125 group = query.group_by_column.value(item)
126 if first || group != previous_group
126 if first || group != previous_group
127 if group.blank? && group != false
127 if group.blank? && group != false
128 group_name = "(#{l(:label_blank_value)})"
128 group_name = "(#{l(:label_blank_value)})"
129 else
129 else
130 group_name = format_object(group)
130 group_name = format_object(group)
131 end
131 end
132 group_name ||= ""
132 group_name ||= ""
133 group_count = item_count_by_group ? item_count_by_group[group] : nil
133 group_count = item_count_by_group ? item_count_by_group[group] : nil
134 group_totals = totals_by_group.map {|column, t| total_tag(column, t[group] || 0)}.join(" ").html_safe
134 group_totals = totals_by_group.map {|column, t| total_tag(column, t[group] || 0)}.join(" ").html_safe
135 end
135 end
136 end
136 end
137 yield item, group_name, group_count, group_totals
137 yield item, group_name, group_count, group_totals
138 previous_group, first = group, false
138 previous_group, first = group, false
139 end
139 end
140 end
140 end
141
141
142 def render_query_totals(query)
142 def render_query_totals(query)
143 return unless query.totalable_columns.present?
143 return unless query.totalable_columns.present?
144 totals = query.totalable_columns.map do |column|
144 totals = query.totalable_columns.map do |column|
145 total_tag(column, query.total_for(column))
145 total_tag(column, query.total_for(column))
146 end
146 end
147 content_tag('p', totals.join(" ").html_safe, :class => "query-totals")
147 content_tag('p', totals.join(" ").html_safe, :class => "query-totals")
148 end
148 end
149
149
150 def total_tag(column, value)
150 def total_tag(column, value)
151 label = content_tag('span', "#{column.caption}:")
151 label = content_tag('span', "#{column.caption}:")
152 value = content_tag('span', format_object(value), :class => 'value')
152 value = content_tag('span', format_object(value), :class => 'value')
153 content_tag('span', label + " " + value, :class => "total-for-#{column.name.to_s.dasherize}")
153 content_tag('span', label + " " + value, :class => "total-for-#{column.name.to_s.dasherize}")
154 end
154 end
155
155
156 def column_header(column)
156 def column_header(column)
157 column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption,
157 column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption,
158 :default_order => column.default_order) :
158 :default_order => column.default_order) :
159 content_tag('th', h(column.caption))
159 content_tag('th', h(column.caption))
160 end
160 end
161
161
162 def column_content(column, issue)
162 def column_content(column, issue)
163 value = column.value_object(issue)
163 value = column.value_object(issue)
164 if value.is_a?(Array)
164 if value.is_a?(Array)
165 value.collect {|v| column_value(column, issue, v)}.compact.join(', ').html_safe
165 value.collect {|v| column_value(column, issue, v)}.compact.join(', ').html_safe
166 else
166 else
167 column_value(column, issue, value)
167 column_value(column, issue, value)
168 end
168 end
169 end
169 end
170
170
171 def column_value(column, issue, value)
171 def column_value(column, issue, value)
172 case column.name
172 case column.name
173 when :id
173 when :id
174 link_to value, issue_path(issue)
174 link_to value, issue_path(issue)
175 when :subject
175 when :subject
176 link_to value, issue_path(issue)
176 link_to value, issue_path(issue)
177 when :parent
177 when :parent
178 value ? (value.visible? ? link_to_issue(value, :subject => false) : "##{value.id}") : ''
178 value ? (value.visible? ? link_to_issue(value, :subject => false) : "##{value.id}") : ''
179 when :description
179 when :description
180 issue.description? ? content_tag('div', textilizable(issue, :description), :class => "wiki") : ''
180 issue.description? ? content_tag('div', textilizable(issue, :description), :class => "wiki") : ''
181 when :done_ratio
181 when :done_ratio
182 progress_bar(value)
182 progress_bar(value)
183 when :relations
183 when :relations
184 content_tag('span',
184 content_tag('span',
185 value.to_s(issue) {|other| link_to_issue(other, :subject => false, :tracker => false)}.html_safe,
185 value.to_s(issue) {|other| link_to_issue(other, :subject => false, :tracker => false)}.html_safe,
186 :class => value.css_classes_for(issue))
186 :class => value.css_classes_for(issue))
187 else
187 else
188 format_object(value)
188 format_object(value)
189 end
189 end
190 end
190 end
191
191
192 def csv_content(column, issue)
192 def csv_content(column, issue)
193 value = column.value_object(issue)
193 value = column.value_object(issue)
194 if value.is_a?(Array)
194 if value.is_a?(Array)
195 value.collect {|v| csv_value(column, issue, v)}.compact.join(', ')
195 value.collect {|v| csv_value(column, issue, v)}.compact.join(', ')
196 else
196 else
197 csv_value(column, issue, value)
197 csv_value(column, issue, value)
198 end
198 end
199 end
199 end
200
200
201 def csv_value(column, object, value)
201 def csv_value(column, object, value)
202 format_object(value, false) do |value|
202 format_object(value, false) do |value|
203 case value.class.name
203 case value.class.name
204 when 'Float'
204 when 'Float'
205 sprintf("%.2f", value).gsub('.', l(:general_csv_decimal_separator))
205 sprintf("%.2f", value).gsub('.', l(:general_csv_decimal_separator))
206 when 'IssueRelation'
206 when 'IssueRelation'
207 value.to_s(object)
207 value.to_s(object)
208 when 'Issue'
208 when 'Issue'
209 if object.is_a?(TimeEntry)
209 if object.is_a?(TimeEntry)
210 "#{value.tracker} ##{value.id}: #{value.subject}"
210 "#{value.tracker} ##{value.id}: #{value.subject}"
211 else
211 else
212 value.id
212 value.id
213 end
213 end
214 else
214 else
215 value
215 value
216 end
216 end
217 end
217 end
218 end
218 end
219
219
220 def query_to_csv(items, query, options={})
220 def query_to_csv(items, query, options={})
221 options ||= {}
221 options ||= {}
222 columns = (options[:columns] == 'all' ? query.available_inline_columns : query.inline_columns)
222 columns = (options[:columns] == 'all' ? query.available_inline_columns : query.inline_columns)
223 query.available_block_columns.each do |column|
223 query.available_block_columns.each do |column|
224 if options[column.name].present?
224 if options[column.name].present?
225 columns << column
225 columns << column
226 end
226 end
227 end
227 end
228
228
229 Redmine::Export::CSV.generate do |csv|
229 Redmine::Export::CSV.generate do |csv|
230 # csv header fields
230 # csv header fields
231 csv << columns.map {|c| c.caption.to_s}
231 csv << columns.map {|c| c.caption.to_s}
232 # csv lines
232 # csv lines
233 items.each do |item|
233 items.each do |item|
234 csv << columns.map {|c| csv_content(c, item)}
234 csv << columns.map {|c| csv_content(c, item)}
235 end
235 end
236 end
236 end
237 end
237 end
238
238
239 # Retrieve query from session or build a new query
239 # Retrieve query from session or build a new query
240 def retrieve_query(klass=IssueQuery, use_session=true)
240 def retrieve_query(klass=IssueQuery, use_session=true)
241 session_key = klass.name.underscore.to_sym
241 session_key = klass.name.underscore.to_sym
242
242
243 if params[:query_id].present?
243 if params[:query_id].present?
244 cond = "project_id IS NULL"
244 cond = "project_id IS NULL"
245 cond << " OR project_id = #{@project.id}" if @project
245 cond << " OR project_id = #{@project.id}" if @project
246 @query = klass.where(cond).find(params[:query_id])
246 @query = klass.where(cond).find(params[:query_id])
247 raise ::Unauthorized unless @query.visible?
247 raise ::Unauthorized unless @query.visible?
248 @query.project = @project
248 @query.project = @project
249 session[session_key] = {:id => @query.id, :project_id => @query.project_id} if use_session
249 session[session_key] = {:id => @query.id, :project_id => @query.project_id} if use_session
250 sort_clear
250 sort_clear
251 elsif api_request? || params[:set_filter] || !use_session || session[session_key].nil? || session[session_key][:project_id] != (@project ? @project.id : nil)
251 elsif api_request? || params[:set_filter] || !use_session || session[session_key].nil? || session[session_key][:project_id] != (@project ? @project.id : nil)
252 # Give it a name, required to be valid
252 # Give it a name, required to be valid
253 @query = klass.new(:name => "_", :project => @project)
253 @query = klass.new(:name => "_", :project => @project)
254 @query.build_from_params(params)
254 @query.build_from_params(params)
255 session[session_key] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names, :totalable_names => @query.totalable_names} if use_session
255 session[session_key] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names, :totalable_names => @query.totalable_names} if use_session
256 else
256 else
257 # retrieve from session
257 # retrieve from session
258 @query = nil
258 @query = nil
259 @query = klass.find_by_id(session[session_key][:id]) if session[session_key][:id]
259 @query = klass.find_by_id(session[session_key][:id]) if session[session_key][:id]
260 @query ||= klass.new(:name => "_", :filters => session[session_key][:filters], :group_by => session[session_key][:group_by], :column_names => session[session_key][:column_names], :totalable_names => session[session_key][:totalable_names])
260 @query ||= klass.new(:name => "_", :filters => session[session_key][:filters], :group_by => session[session_key][:group_by], :column_names => session[session_key][:column_names], :totalable_names => session[session_key][:totalable_names])
261 @query.project = @project
261 @query.project = @project
262 end
262 end
263 end
263 end
264
264
265 def retrieve_query_from_session
265 def retrieve_query_from_session(klass=IssueQuery)
266 if session[:query]
266 session_key = klass.name.underscore.to_sym
267 if session[:query][:id]
267 session_data = session[session_key]
268 @query = IssueQuery.find_by_id(session[:query][:id])
268
269 if session_data
270 if session_data[:id]
271 @query = IssueQuery.find_by_id(session_data[:id])
269 return unless @query
272 return unless @query
270 else
273 else
271 @query = IssueQuery.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names], :totalable_names => session[:query][:totalable_names])
274 @query = IssueQuery.new(:name => "_", :filters => session_data[:filters], :group_by => session_data[:group_by], :column_names => session_data[:column_names], :totalable_names => session_data[:totalable_names])
272 end
275 end
273 if session[:query].has_key?(:project_id)
276 if session_data.has_key?(:project_id)
274 @query.project_id = session[:query][:project_id]
277 @query.project_id = session_data[:project_id]
275 else
278 else
276 @query.project = @project
279 @query.project = @project
277 end
280 end
278 @query
281 @query
279 end
282 end
280 end
283 end
281
284
282 # Returns the query definition as hidden field tags
285 # Returns the query definition as hidden field tags
283 def query_as_hidden_field_tags(query)
286 def query_as_hidden_field_tags(query)
284 tags = hidden_field_tag("set_filter", "1", :id => nil)
287 tags = hidden_field_tag("set_filter", "1", :id => nil)
285
288
286 if query.filters.present?
289 if query.filters.present?
287 query.filters.each do |field, filter|
290 query.filters.each do |field, filter|
288 tags << hidden_field_tag("f[]", field, :id => nil)
291 tags << hidden_field_tag("f[]", field, :id => nil)
289 tags << hidden_field_tag("op[#{field}]", filter[:operator], :id => nil)
292 tags << hidden_field_tag("op[#{field}]", filter[:operator], :id => nil)
290 filter[:values].each do |value|
293 filter[:values].each do |value|
291 tags << hidden_field_tag("v[#{field}][]", value, :id => nil)
294 tags << hidden_field_tag("v[#{field}][]", value, :id => nil)
292 end
295 end
293 end
296 end
294 else
297 else
295 tags << hidden_field_tag("f[]", "", :id => nil)
298 tags << hidden_field_tag("f[]", "", :id => nil)
296 end
299 end
297 if query.column_names.present?
300 if query.column_names.present?
298 query.column_names.each do |name|
301 query.column_names.each do |name|
299 tags << hidden_field_tag("c[]", name, :id => nil)
302 tags << hidden_field_tag("c[]", name, :id => nil)
300 end
303 end
301 end
304 end
302 if query.totalable_names.present?
305 if query.totalable_names.present?
303 query.totalable_names.each do |name|
306 query.totalable_names.each do |name|
304 tags << hidden_field_tag("t[]", name, :id => nil)
307 tags << hidden_field_tag("t[]", name, :id => nil)
305 end
308 end
306 end
309 end
307 if query.group_by.present?
310 if query.group_by.present?
308 tags << hidden_field_tag("group_by", query.group_by, :id => nil)
311 tags << hidden_field_tag("group_by", query.group_by, :id => nil)
309 end
312 end
310
313
311 tags
314 tags
312 end
315 end
313
316
314 # Returns the queries that are rendered in the sidebar
317 # Returns the queries that are rendered in the sidebar
315 def sidebar_queries(klass, project)
318 def sidebar_queries(klass, project)
316 klass.visible.global_or_on_project(@project).sorted.to_a
319 klass.visible.global_or_on_project(@project).sorted.to_a
317 end
320 end
318
321
319 # Renders a group of queries
322 # Renders a group of queries
320 def query_links(title, queries)
323 def query_links(title, queries)
321 return '' if queries.empty?
324 return '' if queries.empty?
322 # links to #index on issues/show
325 # links to #index on issues/show
323 url_params = controller_name == 'issues' ? {:controller => 'issues', :action => 'index', :project_id => @project} : {}
326 url_params = controller_name == 'issues' ? {:controller => 'issues', :action => 'index', :project_id => @project} : {}
324
327
325 content_tag('h3', title) + "\n" +
328 content_tag('h3', title) + "\n" +
326 content_tag('ul',
329 content_tag('ul',
327 queries.collect {|query|
330 queries.collect {|query|
328 css = 'query'
331 css = 'query'
329 css << ' selected' if query == @query
332 css << ' selected' if query == @query
330 content_tag('li', link_to(query.name, url_params.merge(:query_id => query), :class => css))
333 content_tag('li', link_to(query.name, url_params.merge(:query_id => query), :class => css))
331 }.join("\n").html_safe,
334 }.join("\n").html_safe,
332 :class => 'queries'
335 :class => 'queries'
333 ) + "\n"
336 ) + "\n"
334 end
337 end
335
338
336 # Renders the list of queries for the sidebar
339 # Renders the list of queries for the sidebar
337 def render_sidebar_queries(klass, project)
340 def render_sidebar_queries(klass, project)
338 queries = sidebar_queries(klass, project)
341 queries = sidebar_queries(klass, project)
339
342
340 out = ''.html_safe
343 out = ''.html_safe
341 out << query_links(l(:label_my_queries), queries.select(&:is_private?))
344 out << query_links(l(:label_my_queries), queries.select(&:is_private?))
342 out << query_links(l(:label_query_plural), queries.reject(&:is_private?))
345 out << query_links(l(:label_query_plural), queries.reject(&:is_private?))
343 out
346 out
344 end
347 end
345 end
348 end
@@ -1,242 +1,271
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class IssuesTest < Redmine::IntegrationTest
20 class IssuesTest < Redmine::IntegrationTest
21 fixtures :projects,
21 fixtures :projects,
22 :users, :email_addresses,
22 :users, :email_addresses,
23 :roles,
23 :roles,
24 :members,
24 :members,
25 :member_roles,
25 :member_roles,
26 :trackers,
26 :trackers,
27 :projects_trackers,
27 :projects_trackers,
28 :enabled_modules,
28 :enabled_modules,
29 :issue_statuses,
29 :issue_statuses,
30 :issues,
30 :issues,
31 :enumerations,
31 :enumerations,
32 :custom_fields,
32 :custom_fields,
33 :custom_values,
33 :custom_values,
34 :custom_fields_trackers,
34 :custom_fields_trackers,
35 :attachments
35 :attachments
36
36
37 # create an issue
37 # create an issue
38 def test_add_issue
38 def test_add_issue
39 log_user('jsmith', 'jsmith')
39 log_user('jsmith', 'jsmith')
40
40
41 get '/projects/ecookbook/issues/new'
41 get '/projects/ecookbook/issues/new'
42 assert_response :success
42 assert_response :success
43
43
44 issue = new_record(Issue) do
44 issue = new_record(Issue) do
45 post '/projects/ecookbook/issues',
45 post '/projects/ecookbook/issues',
46 :issue => { :tracker_id => "1",
46 :issue => { :tracker_id => "1",
47 :start_date => "2006-12-26",
47 :start_date => "2006-12-26",
48 :priority_id => "4",
48 :priority_id => "4",
49 :subject => "new test issue",
49 :subject => "new test issue",
50 :category_id => "",
50 :category_id => "",
51 :description => "new issue",
51 :description => "new issue",
52 :done_ratio => "0",
52 :done_ratio => "0",
53 :due_date => "",
53 :due_date => "",
54 :assigned_to_id => "" },
54 :assigned_to_id => "" },
55 :custom_fields => {'2' => 'Value for field 2'}
55 :custom_fields => {'2' => 'Value for field 2'}
56 end
56 end
57 # check redirection
57 # check redirection
58 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
58 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
59 follow_redirect!
59 follow_redirect!
60
60
61 # check issue attributes
61 # check issue attributes
62 assert_equal 'jsmith', issue.author.login
62 assert_equal 'jsmith', issue.author.login
63 assert_equal 1, issue.project.id
63 assert_equal 1, issue.project.id
64 assert_equal 1, issue.status.id
64 assert_equal 1, issue.status.id
65 end
65 end
66
66
67 def test_create_issue_by_anonymous_without_permission_should_fail
67 def test_create_issue_by_anonymous_without_permission_should_fail
68 Role.anonymous.remove_permission! :add_issues
68 Role.anonymous.remove_permission! :add_issues
69
69
70 assert_no_difference 'Issue.count' do
70 assert_no_difference 'Issue.count' do
71 post '/projects/1/issues', :tracker_id => "1", :issue => {:subject => "new test issue"}
71 post '/projects/1/issues', :tracker_id => "1", :issue => {:subject => "new test issue"}
72 end
72 end
73 assert_response 302
73 assert_response 302
74 end
74 end
75
75
76 def test_create_issue_by_anonymous_with_custom_permission_should_succeed
76 def test_create_issue_by_anonymous_with_custom_permission_should_succeed
77 Role.anonymous.remove_permission! :add_issues
77 Role.anonymous.remove_permission! :add_issues
78 Member.create!(:project_id => 1, :principal => Group.anonymous, :role_ids => [3])
78 Member.create!(:project_id => 1, :principal => Group.anonymous, :role_ids => [3])
79
79
80 issue = new_record(Issue) do
80 issue = new_record(Issue) do
81 post '/projects/1/issues', :tracker_id => "1", :issue => {:subject => "new test issue"}
81 post '/projects/1/issues', :tracker_id => "1", :issue => {:subject => "new test issue"}
82 assert_response 302
82 assert_response 302
83 end
83 end
84 assert_equal User.anonymous, issue.author
84 assert_equal User.anonymous, issue.author
85 end
85 end
86
86
87 # add then remove 2 attachments to an issue
87 # add then remove 2 attachments to an issue
88 def test_issue_attachments
88 def test_issue_attachments
89 log_user('jsmith', 'jsmith')
89 log_user('jsmith', 'jsmith')
90 set_tmp_attachments_directory
90 set_tmp_attachments_directory
91
91
92 attachment = new_record(Attachment) do
92 attachment = new_record(Attachment) do
93 put '/issues/1',
93 put '/issues/1',
94 :notes => 'Some notes',
94 :notes => 'Some notes',
95 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'This is an attachment'}}
95 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'This is an attachment'}}
96 assert_redirected_to "/issues/1"
96 assert_redirected_to "/issues/1"
97 end
97 end
98
98
99 assert_equal Issue.find(1), attachment.container
99 assert_equal Issue.find(1), attachment.container
100 assert_equal 'testfile.txt', attachment.filename
100 assert_equal 'testfile.txt', attachment.filename
101 assert_equal 'This is an attachment', attachment.description
101 assert_equal 'This is an attachment', attachment.description
102 # verify the size of the attachment stored in db
102 # verify the size of the attachment stored in db
103 #assert_equal file_data_1.length, attachment.filesize
103 #assert_equal file_data_1.length, attachment.filesize
104 # verify that the attachment was written to disk
104 # verify that the attachment was written to disk
105 assert File.exist?(attachment.diskfile)
105 assert File.exist?(attachment.diskfile)
106
106
107 # remove the attachments
107 # remove the attachments
108 Issue.find(1).attachments.each(&:destroy)
108 Issue.find(1).attachments.each(&:destroy)
109 assert_equal 0, Issue.find(1).attachments.length
109 assert_equal 0, Issue.find(1).attachments.length
110 end
110 end
111
111
112 def test_next_and_previous_links_should_be_displayed_after_filter
113 with_settings :default_language => 'en' do
114 get '/projects/ecookbook/issues?set_filter=1&tracker_id=1'
115 assert_response :success
116 assert_select 'td.id', :text => '5'
117
118 get '/issues/5'
119 assert_response :success
120 assert_select '.next-prev-links .position', :text => '3 of 5'
121 end
122 end
123
124 def test_next_and_previous_links_should_be_displayed_after_saved_query
125 query = IssueQuery.create!(:name => 'Calendar Query',
126 :visibility => IssueQuery::VISIBILITY_PUBLIC,
127 :filters => {'tracker_id' => {:operator => '=', :values => ['1']}}
128 )
129
130 with_settings :default_language => 'en' do
131 get "/projects/ecookbook/issues?set_filter=1&query_id=#{query.id}"
132 assert_response :success
133 assert_select 'td.id', :text => '5'
134
135 get '/issues/5'
136 assert_response :success
137 assert_select '.next-prev-links .position', :text => '6 of 8'
138 end
139 end
140
112 def test_other_formats_links_on_index
141 def test_other_formats_links_on_index
113 get '/projects/ecookbook/issues'
142 get '/projects/ecookbook/issues'
114
143
115 %w(Atom PDF CSV).each do |format|
144 %w(Atom PDF CSV).each do |format|
116 assert_select 'a[rel=nofollow][href=?]', "/projects/ecookbook/issues.#{format.downcase}", :text => format
145 assert_select 'a[rel=nofollow][href=?]', "/projects/ecookbook/issues.#{format.downcase}", :text => format
117 end
146 end
118 end
147 end
119
148
120 def test_other_formats_links_on_index_without_project_id_in_url
149 def test_other_formats_links_on_index_without_project_id_in_url
121 get '/issues', :project_id => 'ecookbook'
150 get '/issues', :project_id => 'ecookbook'
122
151
123 %w(Atom PDF CSV).each do |format|
152 %w(Atom PDF CSV).each do |format|
124 assert_select 'a[rel=nofollow][href=?]', "/issues.#{format.downcase}?project_id=ecookbook", :text => format
153 assert_select 'a[rel=nofollow][href=?]', "/issues.#{format.downcase}?project_id=ecookbook", :text => format
125 end
154 end
126 end
155 end
127
156
128 def test_pagination_links_on_index
157 def test_pagination_links_on_index
129 with_settings :per_page_options => '2' do
158 with_settings :per_page_options => '2' do
130 get '/projects/ecookbook/issues'
159 get '/projects/ecookbook/issues'
131
160
132 assert_select 'a[href=?]', '/projects/ecookbook/issues?page=2', :text => '2'
161 assert_select 'a[href=?]', '/projects/ecookbook/issues?page=2', :text => '2'
133 end
162 end
134 end
163 end
135
164
136 def test_pagination_links_should_preserve_query_parameters
165 def test_pagination_links_should_preserve_query_parameters
137 with_settings :per_page_options => '2' do
166 with_settings :per_page_options => '2' do
138 get '/projects/ecookbook/issues?foo=bar'
167 get '/projects/ecookbook/issues?foo=bar'
139
168
140 assert_select 'a[href=?]', '/projects/ecookbook/issues?foo=bar&page=2', :text => '2'
169 assert_select 'a[href=?]', '/projects/ecookbook/issues?foo=bar&page=2', :text => '2'
141 end
170 end
142 end
171 end
143
172
144 def test_pagination_links_should_not_use_params_as_url_options
173 def test_pagination_links_should_not_use_params_as_url_options
145 with_settings :per_page_options => '2' do
174 with_settings :per_page_options => '2' do
146 get '/projects/ecookbook/issues?host=foo'
175 get '/projects/ecookbook/issues?host=foo'
147
176
148 assert_select 'a[href=?]', '/projects/ecookbook/issues?host=foo&page=2', :text => '2'
177 assert_select 'a[href=?]', '/projects/ecookbook/issues?host=foo&page=2', :text => '2'
149 end
178 end
150 end
179 end
151
180
152 def test_sort_links_on_index
181 def test_sort_links_on_index
153 get '/projects/ecookbook/issues'
182 get '/projects/ecookbook/issues'
154
183
155 assert_select 'a[href=?]', '/projects/ecookbook/issues?sort=subject%2Cid%3Adesc', :text => 'Subject'
184 assert_select 'a[href=?]', '/projects/ecookbook/issues?sort=subject%2Cid%3Adesc', :text => 'Subject'
156 end
185 end
157
186
158 def test_sort_links_should_preserve_query_parameters
187 def test_sort_links_should_preserve_query_parameters
159 get '/projects/ecookbook/issues?foo=bar'
188 get '/projects/ecookbook/issues?foo=bar'
160
189
161 assert_select 'a[href=?]', '/projects/ecookbook/issues?foo=bar&sort=subject%2Cid%3Adesc', :text => 'Subject'
190 assert_select 'a[href=?]', '/projects/ecookbook/issues?foo=bar&sort=subject%2Cid%3Adesc', :text => 'Subject'
162 end
191 end
163
192
164 def test_sort_links_should_not_use_params_as_url_options
193 def test_sort_links_should_not_use_params_as_url_options
165 get '/projects/ecookbook/issues?host=foo'
194 get '/projects/ecookbook/issues?host=foo'
166
195
167 assert_select 'a[href=?]', '/projects/ecookbook/issues?host=foo&sort=subject%2Cid%3Adesc', :text => 'Subject'
196 assert_select 'a[href=?]', '/projects/ecookbook/issues?host=foo&sort=subject%2Cid%3Adesc', :text => 'Subject'
168 end
197 end
169
198
170 def test_issue_with_user_custom_field
199 def test_issue_with_user_custom_field
171 @field = IssueCustomField.create!(:name => 'Tester', :field_format => 'user', :is_for_all => true, :trackers => Tracker.all)
200 @field = IssueCustomField.create!(:name => 'Tester', :field_format => 'user', :is_for_all => true, :trackers => Tracker.all)
172 Role.anonymous.add_permission! :add_issues, :edit_issues
201 Role.anonymous.add_permission! :add_issues, :edit_issues
173 users = Project.find(1).users.sort
202 users = Project.find(1).users.sort
174 tester = users.first
203 tester = users.first
175
204
176 # Issue form
205 # Issue form
177 get '/projects/ecookbook/issues/new'
206 get '/projects/ecookbook/issues/new'
178 assert_response :success
207 assert_response :success
179 assert_select 'select[name=?]', "issue[custom_field_values][#{@field.id}]" do
208 assert_select 'select[name=?]', "issue[custom_field_values][#{@field.id}]" do
180 assert_select 'option', users.size + 1 # +1 for blank value
209 assert_select 'option', users.size + 1 # +1 for blank value
181 assert_select 'option[value=?]', tester.id.to_s, :text => tester.name
210 assert_select 'option[value=?]', tester.id.to_s, :text => tester.name
182 end
211 end
183
212
184 # Create issue
213 # Create issue
185 issue = new_record(Issue) do
214 issue = new_record(Issue) do
186 post '/projects/ecookbook/issues',
215 post '/projects/ecookbook/issues',
187 :issue => {
216 :issue => {
188 :tracker_id => '1',
217 :tracker_id => '1',
189 :priority_id => '4',
218 :priority_id => '4',
190 :subject => 'Issue with user custom field',
219 :subject => 'Issue with user custom field',
191 :custom_field_values => {@field.id.to_s => users.first.id.to_s}
220 :custom_field_values => {@field.id.to_s => users.first.id.to_s}
192 }
221 }
193 assert_response 302
222 assert_response 302
194 end
223 end
195
224
196 # Issue view
225 # Issue view
197 follow_redirect!
226 follow_redirect!
198 assert_select ".cf_#{@field.id}" do
227 assert_select ".cf_#{@field.id}" do
199 assert_select '.label', :text => 'Tester:'
228 assert_select '.label', :text => 'Tester:'
200 assert_select '.value', :text => tester.name
229 assert_select '.value', :text => tester.name
201 end
230 end
202 assert_select 'select[name=?]', "issue[custom_field_values][#{@field.id}]" do
231 assert_select 'select[name=?]', "issue[custom_field_values][#{@field.id}]" do
203 assert_select 'option', users.size + 1 # +1 for blank value
232 assert_select 'option', users.size + 1 # +1 for blank value
204 assert_select 'option[value=?][selected=selected]', tester.id.to_s, :text => tester.name
233 assert_select 'option[value=?][selected=selected]', tester.id.to_s, :text => tester.name
205 end
234 end
206
235
207 new_tester = users[1]
236 new_tester = users[1]
208 with_settings :default_language => 'en' do
237 with_settings :default_language => 'en' do
209 # Update issue
238 # Update issue
210 assert_difference 'Journal.count' do
239 assert_difference 'Journal.count' do
211 put "/issues/#{issue.id}",
240 put "/issues/#{issue.id}",
212 :notes => 'Updating custom field',
241 :notes => 'Updating custom field',
213 :issue => {
242 :issue => {
214 :custom_field_values => {@field.id.to_s => new_tester.id.to_s}
243 :custom_field_values => {@field.id.to_s => new_tester.id.to_s}
215 }
244 }
216 assert_redirected_to "/issues/#{issue.id}"
245 assert_redirected_to "/issues/#{issue.id}"
217 end
246 end
218 # Issue view
247 # Issue view
219 follow_redirect!
248 follow_redirect!
220 assert_select 'ul.details li', :text => "Tester changed from #{tester} to #{new_tester}"
249 assert_select 'ul.details li', :text => "Tester changed from #{tester} to #{new_tester}"
221 end
250 end
222 end
251 end
223
252
224 def test_update_using_invalid_http_verbs
253 def test_update_using_invalid_http_verbs
225 subject = 'Updated by an invalid http verb'
254 subject = 'Updated by an invalid http verb'
226
255
227 get '/issues/update/1', {:issue => {:subject => subject}}, credentials('jsmith')
256 get '/issues/update/1', {:issue => {:subject => subject}}, credentials('jsmith')
228 assert_response 404
257 assert_response 404
229 assert_not_equal subject, Issue.find(1).subject
258 assert_not_equal subject, Issue.find(1).subject
230
259
231 post '/issues/1', {:issue => {:subject => subject}}, credentials('jsmith')
260 post '/issues/1', {:issue => {:subject => subject}}, credentials('jsmith')
232 assert_response 404
261 assert_response 404
233 assert_not_equal subject, Issue.find(1).subject
262 assert_not_equal subject, Issue.find(1).subject
234 end
263 end
235
264
236 def test_get_watch_should_be_invalid
265 def test_get_watch_should_be_invalid
237 assert_no_difference 'Watcher.count' do
266 assert_no_difference 'Watcher.count' do
238 get '/watchers/watch?object_type=issue&object_id=1', {}, credentials('jsmith')
267 get '/watchers/watch?object_type=issue&object_id=1', {}, credentials('jsmith')
239 assert_response 404
268 assert_response 404
240 end
269 end
241 end
270 end
242 end
271 end
General Comments 0
You need to be logged in to leave comments. Login now