##// END OF EJS Templates
Adds a sortable "Project" column to the issue list....
Jean-Philippe Lang -
r2498:03572ec5692c
parent child
Show More
@@ -1,55 +1,55
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 module QueriesHelper
19 19
20 20 def operators_for_select(filter_type)
21 21 Query.operators_by_filter_type[filter_type].collect {|o| [l(Query.operators[o]), o]}
22 22 end
23 23
24 24 def column_header(column)
25 25 column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption,
26 26 :default_order => column.default_order) :
27 27 content_tag('th', column.caption)
28 28 end
29 29
30 30 def column_content(column, issue)
31 31 if column.is_a?(QueryCustomFieldColumn)
32 32 cv = issue.custom_values.detect {|v| v.custom_field_id == column.custom_field.id}
33 33 show_value(cv)
34 34 else
35 35 value = issue.send(column.name)
36 36 if value.is_a?(Date)
37 37 format_date(value)
38 38 elsif value.is_a?(Time)
39 39 format_time(value)
40 40 else
41 41 case column.name
42 42 when :subject
43 h((@project.nil? || @project != issue.project) ? "#{issue.project.name} - " : '') +
43 h((!@project.nil? && @project != issue.project) ? "#{issue.project.name} - " : '') +
44 44 link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
45 45 when :done_ratio
46 46 progress_bar(value, :width => '80px')
47 47 when :fixed_version
48 48 link_to(h(value), { :controller => 'versions', :action => 'show', :id => issue.fixed_version_id })
49 49 else
50 50 h(value)
51 51 end
52 52 end
53 53 end
54 54 end
55 55 end
@@ -1,411 +1,415
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class QueryColumn
19 19 attr_accessor :name, :sortable, :default_order
20 20 include Redmine::I18n
21 21
22 22 def initialize(name, options={})
23 23 self.name = name
24 24 self.sortable = options[:sortable]
25 25 self.default_order = options[:default_order]
26 26 end
27 27
28 28 def caption
29 29 l("field_#{name}")
30 30 end
31 31 end
32 32
33 33 class QueryCustomFieldColumn < QueryColumn
34 34
35 35 def initialize(custom_field)
36 36 self.name = "cf_#{custom_field.id}".to_sym
37 37 self.sortable = custom_field.order_statement || false
38 38 @cf = custom_field
39 39 end
40 40
41 41 def caption
42 42 @cf.name
43 43 end
44 44
45 45 def custom_field
46 46 @cf
47 47 end
48 48 end
49 49
50 50 class Query < ActiveRecord::Base
51 51 belongs_to :project
52 52 belongs_to :user
53 53 serialize :filters
54 54 serialize :column_names
55 55
56 56 attr_protected :project_id, :user_id
57 57
58 58 validates_presence_of :name, :on => :save
59 59 validates_length_of :name, :maximum => 255
60 60
61 61 @@operators = { "=" => :label_equals,
62 62 "!" => :label_not_equals,
63 63 "o" => :label_open_issues,
64 64 "c" => :label_closed_issues,
65 65 "!*" => :label_none,
66 66 "*" => :label_all,
67 67 ">=" => '>=',
68 68 "<=" => '<=',
69 69 "<t+" => :label_in_less_than,
70 70 ">t+" => :label_in_more_than,
71 71 "t+" => :label_in,
72 72 "t" => :label_today,
73 73 "w" => :label_this_week,
74 74 ">t-" => :label_less_than_ago,
75 75 "<t-" => :label_more_than_ago,
76 76 "t-" => :label_ago,
77 77 "~" => :label_contains,
78 78 "!~" => :label_not_contains }
79 79
80 80 cattr_reader :operators
81 81
82 82 @@operators_by_filter_type = { :list => [ "=", "!" ],
83 83 :list_status => [ "o", "=", "!", "c", "*" ],
84 84 :list_optional => [ "=", "!", "!*", "*" ],
85 85 :list_subprojects => [ "*", "!*", "=" ],
86 86 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
87 87 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
88 88 :string => [ "=", "~", "!", "!~" ],
89 89 :text => [ "~", "!~" ],
90 90 :integer => [ "=", ">=", "<=", "!*", "*" ] }
91 91
92 92 cattr_reader :operators_by_filter_type
93 93
94 94 @@available_columns = [
95 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name"),
95 96 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position"),
96 97 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position"),
97 98 QueryColumn.new(:priority, :sortable => "#{Enumeration.table_name}.position", :default_order => 'desc'),
98 99 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
99 100 QueryColumn.new(:author),
100 101 QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname"]),
101 102 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
102 103 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name"),
103 104 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc'),
104 105 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
105 106 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
106 107 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
107 108 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio"),
108 109 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
109 110 ]
110 111 cattr_reader :available_columns
111 112
112 113 def initialize(attributes = nil)
113 114 super attributes
114 115 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
115 116 end
116 117
117 118 def after_initialize
118 119 # Store the fact that project is nil (used in #editable_by?)
119 120 @is_for_all = project.nil?
120 121 end
121 122
122 123 def validate
123 124 filters.each_key do |field|
124 125 errors.add label_for(field), :blank unless
125 126 # filter requires one or more values
126 127 (values_for(field) and !values_for(field).first.blank?) or
127 128 # filter doesn't require any value
128 129 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
129 130 end if filters
130 131 end
131 132
132 133 def editable_by?(user)
133 134 return false unless user
134 135 # Admin can edit them all and regular users can edit their private queries
135 136 return true if user.admin? || (!is_public && self.user_id == user.id)
136 137 # Members can not edit public queries that are for all project (only admin is allowed to)
137 138 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
138 139 end
139 140
140 141 def available_filters
141 142 return @available_filters if @available_filters
142 143
143 144 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
144 145
145 146 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
146 147 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
147 148 "priority_id" => { :type => :list, :order => 3, :values => Enumeration.find(:all, :conditions => ['opt=?','IPRI'], :order => 'position').collect{|s| [s.name, s.id.to_s] } },
148 149 "subject" => { :type => :text, :order => 8 },
149 150 "created_on" => { :type => :date_past, :order => 9 },
150 151 "updated_on" => { :type => :date_past, :order => 10 },
151 152 "start_date" => { :type => :date, :order => 11 },
152 153 "due_date" => { :type => :date, :order => 12 },
153 154 "estimated_hours" => { :type => :integer, :order => 13 },
154 155 "done_ratio" => { :type => :integer, :order => 14 }}
155 156
156 157 user_values = []
157 158 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
158 159 if project
159 160 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
160 161 else
161 162 # members of the user's projects
162 163 user_values += User.current.projects.collect(&:users).flatten.uniq.sort.collect{|s| [s.name, s.id.to_s] }
163 164 end
164 165 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
165 166 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
166 167
167 168 if User.current.logged?
168 169 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
169 170 end
170 171
171 172 if project
172 173 # project specific filters
173 174 unless @project.issue_categories.empty?
174 175 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
175 176 end
176 177 unless @project.versions.empty?
177 178 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } }
178 179 end
179 180 unless @project.descendants.active.empty?
180 181 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } }
181 182 end
182 183 add_custom_fields_filters(@project.all_issue_custom_fields)
183 184 else
184 185 # global filters for cross project issue list
185 186 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
186 187 end
187 188 @available_filters
188 189 end
189 190
190 191 def add_filter(field, operator, values)
191 192 # values must be an array
192 193 return unless values and values.is_a? Array # and !values.first.empty?
193 194 # check if field is defined as an available filter
194 195 if available_filters.has_key? field
195 196 filter_options = available_filters[field]
196 197 # check if operator is allowed for that filter
197 198 #if @@operators_by_filter_type[filter_options[:type]].include? operator
198 199 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
199 200 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
200 201 #end
201 202 filters[field] = {:operator => operator, :values => values }
202 203 end
203 204 end
204 205
205 206 def add_short_filter(field, expression)
206 207 return unless expression
207 208 parms = expression.scan(/^(o|c|\!|\*)?(.*)$/).first
208 209 add_filter field, (parms[0] || "="), [parms[1] || ""]
209 210 end
210 211
211 212 def has_filter?(field)
212 213 filters and filters[field]
213 214 end
214 215
215 216 def operator_for(field)
216 217 has_filter?(field) ? filters[field][:operator] : nil
217 218 end
218 219
219 220 def values_for(field)
220 221 has_filter?(field) ? filters[field][:values] : nil
221 222 end
222 223
223 224 def label_for(field)
224 225 label = available_filters[field][:name] if available_filters.has_key?(field)
225 226 label ||= field.gsub(/\_id$/, "")
226 227 end
227 228
228 229 def available_columns
229 230 return @available_columns if @available_columns
230 231 @available_columns = Query.available_columns
231 232 @available_columns += (project ?
232 233 project.all_issue_custom_fields :
233 234 IssueCustomField.find(:all, :conditions => {:is_for_all => true})
234 235 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
235 236 end
236 237
237 238 def columns
238 239 if has_default_columns?
239 available_columns.select {|c| Setting.issue_list_default_columns.include?(c.name.to_s) }
240 available_columns.select do |c|
241 # Adds the project column by default for cross-project lists
242 Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?)
243 end
240 244 else
241 245 # preserve the column_names order
242 246 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
243 247 end
244 248 end
245 249
246 250 def column_names=(names)
247 251 names = names.select {|n| n.is_a?(Symbol) || !n.blank? } if names
248 252 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym } if names
249 253 write_attribute(:column_names, names)
250 254 end
251 255
252 256 def has_column?(column)
253 257 column_names && column_names.include?(column.name)
254 258 end
255 259
256 260 def has_default_columns?
257 261 column_names.nil? || column_names.empty?
258 262 end
259 263
260 264 def project_statement
261 265 project_clauses = []
262 266 if project && !@project.descendants.active.empty?
263 267 ids = [project.id]
264 268 if has_filter?("subproject_id")
265 269 case operator_for("subproject_id")
266 270 when '='
267 271 # include the selected subprojects
268 272 ids += values_for("subproject_id").each(&:to_i)
269 273 when '!*'
270 274 # main project only
271 275 else
272 276 # all subprojects
273 277 ids += project.descendants.collect(&:id)
274 278 end
275 279 elsif Setting.display_subprojects_issues?
276 280 ids += project.descendants.collect(&:id)
277 281 end
278 282 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
279 283 elsif project
280 284 project_clauses << "#{Project.table_name}.id = %d" % project.id
281 285 end
282 286 project_clauses << Project.allowed_to_condition(User.current, :view_issues)
283 287 project_clauses.join(' AND ')
284 288 end
285 289
286 290 def statement
287 291 # filters clauses
288 292 filters_clauses = []
289 293 filters.each_key do |field|
290 294 next if field == "subproject_id"
291 295 v = values_for(field).clone
292 296 next unless v and !v.empty?
293 297 operator = operator_for(field)
294 298
295 299 # "me" value subsitution
296 300 if %w(assigned_to_id author_id watcher_id).include?(field)
297 301 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
298 302 end
299 303
300 304 sql = ''
301 305 if field =~ /^cf_(\d+)$/
302 306 # custom field
303 307 db_table = CustomValue.table_name
304 308 db_field = 'value'
305 309 is_custom_filter = true
306 310 sql << "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} WHERE "
307 311 sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')'
308 312 elsif field == 'watcher_id'
309 313 db_table = Watcher.table_name
310 314 db_field = 'user_id'
311 315 sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND "
312 316 sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
313 317 else
314 318 # regular field
315 319 db_table = Issue.table_name
316 320 db_field = field
317 321 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
318 322 end
319 323 filters_clauses << sql
320 324
321 325 end if filters and valid?
322 326
323 327 (filters_clauses << project_statement).join(' AND ')
324 328 end
325 329
326 330 private
327 331
328 332 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
329 333 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
330 334 sql = ''
331 335 case operator
332 336 when "="
333 337 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
334 338 when "!"
335 339 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
336 340 when "!*"
337 341 sql = "#{db_table}.#{db_field} IS NULL"
338 342 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
339 343 when "*"
340 344 sql = "#{db_table}.#{db_field} IS NOT NULL"
341 345 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
342 346 when ">="
343 347 sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
344 348 when "<="
345 349 sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
346 350 when "o"
347 351 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
348 352 when "c"
349 353 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
350 354 when ">t-"
351 355 sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
352 356 when "<t-"
353 357 sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
354 358 when "t-"
355 359 sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
356 360 when ">t+"
357 361 sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
358 362 when "<t+"
359 363 sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
360 364 when "t+"
361 365 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
362 366 when "t"
363 367 sql = date_range_clause(db_table, db_field, 0, 0)
364 368 when "w"
365 369 from = l(:general_first_day_of_week) == '7' ?
366 370 # week starts on sunday
367 371 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
368 372 # week starts on monday (Rails default)
369 373 Time.now.at_beginning_of_week
370 374 sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
371 375 when "~"
372 376 sql = "#{db_table}.#{db_field} LIKE '%#{connection.quote_string(value.first)}%'"
373 377 when "!~"
374 378 sql = "#{db_table}.#{db_field} NOT LIKE '%#{connection.quote_string(value.first)}%'"
375 379 end
376 380
377 381 return sql
378 382 end
379 383
380 384 def add_custom_fields_filters(custom_fields)
381 385 @available_filters ||= {}
382 386
383 387 custom_fields.select(&:is_filter?).each do |field|
384 388 case field.field_format
385 389 when "text"
386 390 options = { :type => :text, :order => 20 }
387 391 when "list"
388 392 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
389 393 when "date"
390 394 options = { :type => :date, :order => 20 }
391 395 when "bool"
392 396 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
393 397 else
394 398 options = { :type => :string, :order => 20 }
395 399 end
396 400 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
397 401 end
398 402 end
399 403
400 404 # Returns a SQL clause for a date or datetime field.
401 405 def date_range_clause(table, field, from, to)
402 406 s = []
403 407 if from
404 408 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
405 409 end
406 410 if to
407 411 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
408 412 end
409 413 s.join(' AND ')
410 414 end
411 415 end
@@ -1,985 +1,989
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19 require 'issues_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class IssuesController; def rescue_action(e) raise e end; end
23 23
24 24 class IssuesControllerTest < Test::Unit::TestCase
25 25 fixtures :projects,
26 26 :users,
27 27 :roles,
28 28 :members,
29 29 :issues,
30 30 :issue_statuses,
31 31 :versions,
32 32 :trackers,
33 33 :projects_trackers,
34 34 :issue_categories,
35 35 :enabled_modules,
36 36 :enumerations,
37 37 :attachments,
38 38 :workflows,
39 39 :custom_fields,
40 40 :custom_values,
41 41 :custom_fields_trackers,
42 42 :time_entries,
43 43 :journals,
44 44 :journal_details
45 45
46 46 def setup
47 47 @controller = IssuesController.new
48 48 @request = ActionController::TestRequest.new
49 49 @response = ActionController::TestResponse.new
50 50 User.current = nil
51 51 end
52 52
53 53 def test_index_routing
54 54 assert_routing(
55 55 {:method => :get, :path => '/issues'},
56 56 :controller => 'issues', :action => 'index'
57 57 )
58 58 end
59 59
60 60 def test_index
61 Setting.default_language = 'en'
62
61 63 get :index
62 64 assert_response :success
63 65 assert_template 'index.rhtml'
64 66 assert_not_nil assigns(:issues)
65 67 assert_nil assigns(:project)
66 68 assert_tag :tag => 'a', :content => /Can't print recipes/
67 69 assert_tag :tag => 'a', :content => /Subproject issue/
68 70 # private projects hidden
69 71 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
70 72 assert_no_tag :tag => 'a', :content => /Issue on project 2/
73 # project column
74 assert_tag :tag => 'th', :content => /Project/
71 75 end
72 76
73 77 def test_index_should_not_list_issues_when_module_disabled
74 78 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
75 79 get :index
76 80 assert_response :success
77 81 assert_template 'index.rhtml'
78 82 assert_not_nil assigns(:issues)
79 83 assert_nil assigns(:project)
80 84 assert_no_tag :tag => 'a', :content => /Can't print recipes/
81 85 assert_tag :tag => 'a', :content => /Subproject issue/
82 86 end
83 87
84 88 def test_index_with_project_routing
85 89 assert_routing(
86 90 {:method => :get, :path => '/projects/23/issues'},
87 91 :controller => 'issues', :action => 'index', :project_id => '23'
88 92 )
89 93 end
90 94
91 95 def test_index_should_not_list_issues_when_module_disabled
92 96 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
93 97 get :index
94 98 assert_response :success
95 99 assert_template 'index.rhtml'
96 100 assert_not_nil assigns(:issues)
97 101 assert_nil assigns(:project)
98 102 assert_no_tag :tag => 'a', :content => /Can't print recipes/
99 103 assert_tag :tag => 'a', :content => /Subproject issue/
100 104 end
101 105
102 106 def test_index_with_project_routing
103 107 assert_routing(
104 108 {:method => :get, :path => 'projects/23/issues'},
105 109 :controller => 'issues', :action => 'index', :project_id => '23'
106 110 )
107 111 end
108 112
109 113 def test_index_with_project
110 114 Setting.display_subprojects_issues = 0
111 115 get :index, :project_id => 1
112 116 assert_response :success
113 117 assert_template 'index.rhtml'
114 118 assert_not_nil assigns(:issues)
115 119 assert_tag :tag => 'a', :content => /Can't print recipes/
116 120 assert_no_tag :tag => 'a', :content => /Subproject issue/
117 121 end
118 122
119 123 def test_index_with_project_and_subprojects
120 124 Setting.display_subprojects_issues = 1
121 125 get :index, :project_id => 1
122 126 assert_response :success
123 127 assert_template 'index.rhtml'
124 128 assert_not_nil assigns(:issues)
125 129 assert_tag :tag => 'a', :content => /Can't print recipes/
126 130 assert_tag :tag => 'a', :content => /Subproject issue/
127 131 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
128 132 end
129 133
130 134 def test_index_with_project_and_subprojects_should_show_private_subprojects
131 135 @request.session[:user_id] = 2
132 136 Setting.display_subprojects_issues = 1
133 137 get :index, :project_id => 1
134 138 assert_response :success
135 139 assert_template 'index.rhtml'
136 140 assert_not_nil assigns(:issues)
137 141 assert_tag :tag => 'a', :content => /Can't print recipes/
138 142 assert_tag :tag => 'a', :content => /Subproject issue/
139 143 assert_tag :tag => 'a', :content => /Issue of a private subproject/
140 144 end
141 145
142 146 def test_index_with_project_routing_formatted
143 147 assert_routing(
144 148 {:method => :get, :path => 'projects/23/issues.pdf'},
145 149 :controller => 'issues', :action => 'index', :project_id => '23', :format => 'pdf'
146 150 )
147 151 assert_routing(
148 152 {:method => :get, :path => 'projects/23/issues.atom'},
149 153 :controller => 'issues', :action => 'index', :project_id => '23', :format => 'atom'
150 154 )
151 155 end
152 156
153 157 def test_index_with_project_and_filter
154 158 get :index, :project_id => 1, :set_filter => 1
155 159 assert_response :success
156 160 assert_template 'index.rhtml'
157 161 assert_not_nil assigns(:issues)
158 162 end
159 163
160 164 def test_index_csv_with_project
161 165 get :index, :format => 'csv'
162 166 assert_response :success
163 167 assert_not_nil assigns(:issues)
164 168 assert_equal 'text/csv', @response.content_type
165 169
166 170 get :index, :project_id => 1, :format => 'csv'
167 171 assert_response :success
168 172 assert_not_nil assigns(:issues)
169 173 assert_equal 'text/csv', @response.content_type
170 174 end
171 175
172 176 def test_index_formatted
173 177 assert_routing(
174 178 {:method => :get, :path => 'issues.pdf'},
175 179 :controller => 'issues', :action => 'index', :format => 'pdf'
176 180 )
177 181 assert_routing(
178 182 {:method => :get, :path => 'issues.atom'},
179 183 :controller => 'issues', :action => 'index', :format => 'atom'
180 184 )
181 185 end
182 186
183 187 def test_index_pdf
184 188 get :index, :format => 'pdf'
185 189 assert_response :success
186 190 assert_not_nil assigns(:issues)
187 191 assert_equal 'application/pdf', @response.content_type
188 192
189 193 get :index, :project_id => 1, :format => 'pdf'
190 194 assert_response :success
191 195 assert_not_nil assigns(:issues)
192 196 assert_equal 'application/pdf', @response.content_type
193 197 end
194 198
195 199 def test_index_sort
196 200 get :index, :sort_key => 'tracker'
197 201 assert_response :success
198 202
199 203 sort_params = @request.session['issuesindex_sort']
200 204 assert sort_params.is_a?(Hash)
201 205 assert_equal 'tracker', sort_params[:key]
202 206 assert_equal 'ASC', sort_params[:order]
203 207 end
204 208
205 209 def test_gantt
206 210 get :gantt, :project_id => 1
207 211 assert_response :success
208 212 assert_template 'gantt.rhtml'
209 213 assert_not_nil assigns(:gantt)
210 214 events = assigns(:gantt).events
211 215 assert_not_nil events
212 216 # Issue with start and due dates
213 217 i = Issue.find(1)
214 218 assert_not_nil i.due_date
215 219 assert events.include?(Issue.find(1))
216 220 # Issue with without due date but targeted to a version with date
217 221 i = Issue.find(2)
218 222 assert_nil i.due_date
219 223 assert events.include?(i)
220 224 end
221 225
222 226 def test_cross_project_gantt
223 227 get :gantt
224 228 assert_response :success
225 229 assert_template 'gantt.rhtml'
226 230 assert_not_nil assigns(:gantt)
227 231 events = assigns(:gantt).events
228 232 assert_not_nil events
229 233 end
230 234
231 235 def test_gantt_export_to_pdf
232 236 get :gantt, :project_id => 1, :format => 'pdf'
233 237 assert_response :success
234 238 assert_equal 'application/pdf', @response.content_type
235 239 assert @response.body.starts_with?('%PDF')
236 240 assert_not_nil assigns(:gantt)
237 241 end
238 242
239 243 def test_cross_project_gantt_export_to_pdf
240 244 get :gantt, :format => 'pdf'
241 245 assert_response :success
242 246 assert_equal 'application/pdf', @response.content_type
243 247 assert @response.body.starts_with?('%PDF')
244 248 assert_not_nil assigns(:gantt)
245 249 end
246 250
247 251 if Object.const_defined?(:Magick)
248 252 def test_gantt_image
249 253 get :gantt, :project_id => 1, :format => 'png'
250 254 assert_response :success
251 255 assert_equal 'image/png', @response.content_type
252 256 end
253 257 else
254 258 puts "RMagick not installed. Skipping tests !!!"
255 259 end
256 260
257 261 def test_calendar
258 262 get :calendar, :project_id => 1
259 263 assert_response :success
260 264 assert_template 'calendar'
261 265 assert_not_nil assigns(:calendar)
262 266 end
263 267
264 268 def test_cross_project_calendar
265 269 get :calendar
266 270 assert_response :success
267 271 assert_template 'calendar'
268 272 assert_not_nil assigns(:calendar)
269 273 end
270 274
271 275 def test_changes
272 276 get :changes, :project_id => 1
273 277 assert_response :success
274 278 assert_not_nil assigns(:journals)
275 279 assert_equal 'application/atom+xml', @response.content_type
276 280 end
277 281
278 282 def test_show_routing
279 283 assert_routing(
280 284 {:method => :get, :path => '/issues/64'},
281 285 :controller => 'issues', :action => 'show', :id => '64'
282 286 )
283 287 end
284 288
285 289 def test_show_routing_formatted
286 290 assert_routing(
287 291 {:method => :get, :path => '/issues/2332.pdf'},
288 292 :controller => 'issues', :action => 'show', :id => '2332', :format => 'pdf'
289 293 )
290 294 assert_routing(
291 295 {:method => :get, :path => '/issues/23123.atom'},
292 296 :controller => 'issues', :action => 'show', :id => '23123', :format => 'atom'
293 297 )
294 298 end
295 299
296 300 def test_show_by_anonymous
297 301 get :show, :id => 1
298 302 assert_response :success
299 303 assert_template 'show.rhtml'
300 304 assert_not_nil assigns(:issue)
301 305 assert_equal Issue.find(1), assigns(:issue)
302 306
303 307 # anonymous role is allowed to add a note
304 308 assert_tag :tag => 'form',
305 309 :descendant => { :tag => 'fieldset',
306 310 :child => { :tag => 'legend',
307 311 :content => /Notes/ } }
308 312 end
309 313
310 314 def test_show_by_manager
311 315 @request.session[:user_id] = 2
312 316 get :show, :id => 1
313 317 assert_response :success
314 318
315 319 assert_tag :tag => 'form',
316 320 :descendant => { :tag => 'fieldset',
317 321 :child => { :tag => 'legend',
318 322 :content => /Change properties/ } },
319 323 :descendant => { :tag => 'fieldset',
320 324 :child => { :tag => 'legend',
321 325 :content => /Log time/ } },
322 326 :descendant => { :tag => 'fieldset',
323 327 :child => { :tag => 'legend',
324 328 :content => /Notes/ } }
325 329 end
326 330
327 331 def test_show_should_not_disclose_relations_to_invisible_issues
328 332 Setting.cross_project_issue_relations = '1'
329 333 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
330 334 # Relation to a private project issue
331 335 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates')
332 336
333 337 get :show, :id => 1
334 338 assert_response :success
335 339
336 340 assert_tag :div, :attributes => { :id => 'relations' },
337 341 :descendant => { :tag => 'a', :content => /#2$/ }
338 342 assert_no_tag :div, :attributes => { :id => 'relations' },
339 343 :descendant => { :tag => 'a', :content => /#4$/ }
340 344 end
341 345
342 346 def test_new_routing
343 347 assert_routing(
344 348 {:method => :get, :path => '/projects/1/issues/new'},
345 349 :controller => 'issues', :action => 'new', :project_id => '1'
346 350 )
347 351 assert_recognizes(
348 352 {:controller => 'issues', :action => 'new', :project_id => '1'},
349 353 {:method => :post, :path => '/projects/1/issues'}
350 354 )
351 355 end
352 356
353 357 def test_show_export_to_pdf
354 358 get :show, :id => 3, :format => 'pdf'
355 359 assert_response :success
356 360 assert_equal 'application/pdf', @response.content_type
357 361 assert @response.body.starts_with?('%PDF')
358 362 assert_not_nil assigns(:issue)
359 363 end
360 364
361 365 def test_get_new
362 366 @request.session[:user_id] = 2
363 367 get :new, :project_id => 1, :tracker_id => 1
364 368 assert_response :success
365 369 assert_template 'new'
366 370
367 371 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
368 372 :value => 'Default string' }
369 373 end
370 374
371 375 def test_get_new_without_tracker_id
372 376 @request.session[:user_id] = 2
373 377 get :new, :project_id => 1
374 378 assert_response :success
375 379 assert_template 'new'
376 380
377 381 issue = assigns(:issue)
378 382 assert_not_nil issue
379 383 assert_equal Project.find(1).trackers.first, issue.tracker
380 384 end
381 385
382 386 def test_get_new_with_no_default_status_should_display_an_error
383 387 @request.session[:user_id] = 2
384 388 IssueStatus.delete_all
385 389
386 390 get :new, :project_id => 1
387 391 assert_response 500
388 392 assert_not_nil flash[:error]
389 393 assert_tag :tag => 'div', :attributes => { :class => /error/ },
390 394 :content => /No default issue/
391 395 end
392 396
393 397 def test_get_new_with_no_tracker_should_display_an_error
394 398 @request.session[:user_id] = 2
395 399 Tracker.delete_all
396 400
397 401 get :new, :project_id => 1
398 402 assert_response 500
399 403 assert_not_nil flash[:error]
400 404 assert_tag :tag => 'div', :attributes => { :class => /error/ },
401 405 :content => /No tracker/
402 406 end
403 407
404 408 def test_update_new_form
405 409 @request.session[:user_id] = 2
406 410 xhr :post, :new, :project_id => 1,
407 411 :issue => {:tracker_id => 2,
408 412 :subject => 'This is the test_new issue',
409 413 :description => 'This is the description',
410 414 :priority_id => 5}
411 415 assert_response :success
412 416 assert_template 'new'
413 417 end
414 418
415 419 def test_post_new
416 420 @request.session[:user_id] = 2
417 421 post :new, :project_id => 1,
418 422 :issue => {:tracker_id => 3,
419 423 :subject => 'This is the test_new issue',
420 424 :description => 'This is the description',
421 425 :priority_id => 5,
422 426 :estimated_hours => '',
423 427 :custom_field_values => {'2' => 'Value for field 2'}}
424 428 assert_redirected_to :action => 'show'
425 429
426 430 issue = Issue.find_by_subject('This is the test_new issue')
427 431 assert_not_nil issue
428 432 assert_equal 2, issue.author_id
429 433 assert_equal 3, issue.tracker_id
430 434 assert_nil issue.estimated_hours
431 435 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
432 436 assert_not_nil v
433 437 assert_equal 'Value for field 2', v.value
434 438 end
435 439
436 440 def test_post_new_and_continue
437 441 @request.session[:user_id] = 2
438 442 post :new, :project_id => 1,
439 443 :issue => {:tracker_id => 3,
440 444 :subject => 'This is first issue',
441 445 :priority_id => 5},
442 446 :continue => ''
443 447 assert_redirected_to :controller => 'issues', :action => 'new', :tracker_id => 3
444 448 end
445 449
446 450 def test_post_new_without_custom_fields_param
447 451 @request.session[:user_id] = 2
448 452 post :new, :project_id => 1,
449 453 :issue => {:tracker_id => 1,
450 454 :subject => 'This is the test_new issue',
451 455 :description => 'This is the description',
452 456 :priority_id => 5}
453 457 assert_redirected_to :action => 'show'
454 458 end
455 459
456 460 def test_post_new_with_required_custom_field_and_without_custom_fields_param
457 461 field = IssueCustomField.find_by_name('Database')
458 462 field.update_attribute(:is_required, true)
459 463
460 464 @request.session[:user_id] = 2
461 465 post :new, :project_id => 1,
462 466 :issue => {:tracker_id => 1,
463 467 :subject => 'This is the test_new issue',
464 468 :description => 'This is the description',
465 469 :priority_id => 5}
466 470 assert_response :success
467 471 assert_template 'new'
468 472 issue = assigns(:issue)
469 473 assert_not_nil issue
470 474 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
471 475 end
472 476
473 477 def test_post_new_with_watchers
474 478 @request.session[:user_id] = 2
475 479 ActionMailer::Base.deliveries.clear
476 480
477 481 assert_difference 'Watcher.count', 2 do
478 482 post :new, :project_id => 1,
479 483 :issue => {:tracker_id => 1,
480 484 :subject => 'This is a new issue with watchers',
481 485 :description => 'This is the description',
482 486 :priority_id => 5,
483 487 :watcher_user_ids => ['2', '3']}
484 488 end
485 489 issue = Issue.find_by_subject('This is a new issue with watchers')
486 490 assert_not_nil issue
487 491 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
488 492
489 493 # Watchers added
490 494 assert_equal [2, 3], issue.watcher_user_ids.sort
491 495 assert issue.watched_by?(User.find(3))
492 496 # Watchers notified
493 497 mail = ActionMailer::Base.deliveries.last
494 498 assert_kind_of TMail::Mail, mail
495 499 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
496 500 end
497 501
498 502 def test_post_should_preserve_fields_values_on_validation_failure
499 503 @request.session[:user_id] = 2
500 504 post :new, :project_id => 1,
501 505 :issue => {:tracker_id => 1,
502 506 # empty subject
503 507 :subject => '',
504 508 :description => 'This is a description',
505 509 :priority_id => 6,
506 510 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
507 511 assert_response :success
508 512 assert_template 'new'
509 513
510 514 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
511 515 :content => 'This is a description'
512 516 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
513 517 :child => { :tag => 'option', :attributes => { :selected => 'selected',
514 518 :value => '6' },
515 519 :content => 'High' }
516 520 # Custom fields
517 521 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
518 522 :child => { :tag => 'option', :attributes => { :selected => 'selected',
519 523 :value => 'Oracle' },
520 524 :content => 'Oracle' }
521 525 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
522 526 :value => 'Value for field 2'}
523 527 end
524 528
525 529 def test_copy_routing
526 530 assert_routing(
527 531 {:method => :get, :path => '/projects/world_domination/issues/567/copy'},
528 532 :controller => 'issues', :action => 'new', :project_id => 'world_domination', :copy_from => '567'
529 533 )
530 534 end
531 535
532 536 def test_copy_issue
533 537 @request.session[:user_id] = 2
534 538 get :new, :project_id => 1, :copy_from => 1
535 539 assert_template 'new'
536 540 assert_not_nil assigns(:issue)
537 541 orig = Issue.find(1)
538 542 assert_equal orig.subject, assigns(:issue).subject
539 543 end
540 544
541 545 def test_edit_routing
542 546 assert_routing(
543 547 {:method => :get, :path => '/issues/1/edit'},
544 548 :controller => 'issues', :action => 'edit', :id => '1'
545 549 )
546 550 assert_recognizes( #TODO: use a PUT on the issue URI isntead, need to adjust form
547 551 {:controller => 'issues', :action => 'edit', :id => '1'},
548 552 {:method => :post, :path => '/issues/1/edit'}
549 553 )
550 554 end
551 555
552 556 def test_get_edit
553 557 @request.session[:user_id] = 2
554 558 get :edit, :id => 1
555 559 assert_response :success
556 560 assert_template 'edit'
557 561 assert_not_nil assigns(:issue)
558 562 assert_equal Issue.find(1), assigns(:issue)
559 563 end
560 564
561 565 def test_get_edit_with_params
562 566 @request.session[:user_id] = 2
563 567 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
564 568 assert_response :success
565 569 assert_template 'edit'
566 570
567 571 issue = assigns(:issue)
568 572 assert_not_nil issue
569 573
570 574 assert_equal 5, issue.status_id
571 575 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
572 576 :child => { :tag => 'option',
573 577 :content => 'Closed',
574 578 :attributes => { :selected => 'selected' } }
575 579
576 580 assert_equal 7, issue.priority_id
577 581 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
578 582 :child => { :tag => 'option',
579 583 :content => 'Urgent',
580 584 :attributes => { :selected => 'selected' } }
581 585 end
582 586
583 587 def test_reply_routing
584 588 assert_routing(
585 589 {:method => :post, :path => '/issues/1/quoted'},
586 590 :controller => 'issues', :action => 'reply', :id => '1'
587 591 )
588 592 end
589 593
590 594 def test_reply_to_issue
591 595 @request.session[:user_id] = 2
592 596 get :reply, :id => 1
593 597 assert_response :success
594 598 assert_select_rjs :show, "update"
595 599 end
596 600
597 601 def test_reply_to_note
598 602 @request.session[:user_id] = 2
599 603 get :reply, :id => 1, :journal_id => 2
600 604 assert_response :success
601 605 assert_select_rjs :show, "update"
602 606 end
603 607
604 608 def test_post_edit_without_custom_fields_param
605 609 @request.session[:user_id] = 2
606 610 ActionMailer::Base.deliveries.clear
607 611
608 612 issue = Issue.find(1)
609 613 assert_equal '125', issue.custom_value_for(2).value
610 614 old_subject = issue.subject
611 615 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
612 616
613 617 assert_difference('Journal.count') do
614 618 assert_difference('JournalDetail.count', 2) do
615 619 post :edit, :id => 1, :issue => {:subject => new_subject,
616 620 :priority_id => '6',
617 621 :category_id => '1' # no change
618 622 }
619 623 end
620 624 end
621 625 assert_redirected_to :action => 'show', :id => '1'
622 626 issue.reload
623 627 assert_equal new_subject, issue.subject
624 628 # Make sure custom fields were not cleared
625 629 assert_equal '125', issue.custom_value_for(2).value
626 630
627 631 mail = ActionMailer::Base.deliveries.last
628 632 assert_kind_of TMail::Mail, mail
629 633 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
630 634 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
631 635 end
632 636
633 637 def test_post_edit_with_custom_field_change
634 638 @request.session[:user_id] = 2
635 639 issue = Issue.find(1)
636 640 assert_equal '125', issue.custom_value_for(2).value
637 641
638 642 assert_difference('Journal.count') do
639 643 assert_difference('JournalDetail.count', 3) do
640 644 post :edit, :id => 1, :issue => {:subject => 'Custom field change',
641 645 :priority_id => '6',
642 646 :category_id => '1', # no change
643 647 :custom_field_values => { '2' => 'New custom value' }
644 648 }
645 649 end
646 650 end
647 651 assert_redirected_to :action => 'show', :id => '1'
648 652 issue.reload
649 653 assert_equal 'New custom value', issue.custom_value_for(2).value
650 654
651 655 mail = ActionMailer::Base.deliveries.last
652 656 assert_kind_of TMail::Mail, mail
653 657 assert mail.body.include?("Searchable field changed from 125 to New custom value")
654 658 end
655 659
656 660 def test_post_edit_with_status_and_assignee_change
657 661 issue = Issue.find(1)
658 662 assert_equal 1, issue.status_id
659 663 @request.session[:user_id] = 2
660 664 assert_difference('TimeEntry.count', 0) do
661 665 post :edit,
662 666 :id => 1,
663 667 :issue => { :status_id => 2, :assigned_to_id => 3 },
664 668 :notes => 'Assigned to dlopper',
665 669 :time_entry => { :hours => '', :comments => '', :activity_id => Enumeration.activities.first }
666 670 end
667 671 assert_redirected_to :action => 'show', :id => '1'
668 672 issue.reload
669 673 assert_equal 2, issue.status_id
670 674 j = issue.journals.find(:first, :order => 'id DESC')
671 675 assert_equal 'Assigned to dlopper', j.notes
672 676 assert_equal 2, j.details.size
673 677
674 678 mail = ActionMailer::Base.deliveries.last
675 679 assert mail.body.include?("Status changed from New to Assigned")
676 680 end
677 681
678 682 def test_post_edit_with_note_only
679 683 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
680 684 # anonymous user
681 685 post :edit,
682 686 :id => 1,
683 687 :notes => notes
684 688 assert_redirected_to :action => 'show', :id => '1'
685 689 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
686 690 assert_equal notes, j.notes
687 691 assert_equal 0, j.details.size
688 692 assert_equal User.anonymous, j.user
689 693
690 694 mail = ActionMailer::Base.deliveries.last
691 695 assert mail.body.include?(notes)
692 696 end
693 697
694 698 def test_post_edit_with_note_and_spent_time
695 699 @request.session[:user_id] = 2
696 700 spent_hours_before = Issue.find(1).spent_hours
697 701 assert_difference('TimeEntry.count') do
698 702 post :edit,
699 703 :id => 1,
700 704 :notes => '2.5 hours added',
701 705 :time_entry => { :hours => '2.5', :comments => '', :activity_id => Enumeration.activities.first }
702 706 end
703 707 assert_redirected_to :action => 'show', :id => '1'
704 708
705 709 issue = Issue.find(1)
706 710
707 711 j = issue.journals.find(:first, :order => 'id DESC')
708 712 assert_equal '2.5 hours added', j.notes
709 713 assert_equal 0, j.details.size
710 714
711 715 t = issue.time_entries.find(:first, :order => 'id DESC')
712 716 assert_not_nil t
713 717 assert_equal 2.5, t.hours
714 718 assert_equal spent_hours_before + 2.5, issue.spent_hours
715 719 end
716 720
717 721 def test_post_edit_with_attachment_only
718 722 set_tmp_attachments_directory
719 723
720 724 # Delete all fixtured journals, a race condition can occur causing the wrong
721 725 # journal to get fetched in the next find.
722 726 Journal.delete_all
723 727
724 728 # anonymous user
725 729 post :edit,
726 730 :id => 1,
727 731 :notes => '',
728 732 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
729 733 assert_redirected_to :action => 'show', :id => '1'
730 734 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
731 735 assert j.notes.blank?
732 736 assert_equal 1, j.details.size
733 737 assert_equal 'testfile.txt', j.details.first.value
734 738 assert_equal User.anonymous, j.user
735 739
736 740 mail = ActionMailer::Base.deliveries.last
737 741 assert mail.body.include?('testfile.txt')
738 742 end
739 743
740 744 def test_post_edit_with_no_change
741 745 issue = Issue.find(1)
742 746 issue.journals.clear
743 747 ActionMailer::Base.deliveries.clear
744 748
745 749 post :edit,
746 750 :id => 1,
747 751 :notes => ''
748 752 assert_redirected_to :action => 'show', :id => '1'
749 753
750 754 issue.reload
751 755 assert issue.journals.empty?
752 756 # No email should be sent
753 757 assert ActionMailer::Base.deliveries.empty?
754 758 end
755 759
756 760 def test_post_edit_with_invalid_spent_time
757 761 @request.session[:user_id] = 2
758 762 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
759 763
760 764 assert_no_difference('Journal.count') do
761 765 post :edit,
762 766 :id => 1,
763 767 :notes => notes,
764 768 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
765 769 end
766 770 assert_response :success
767 771 assert_template 'edit'
768 772
769 773 assert_tag :textarea, :attributes => { :name => 'notes' },
770 774 :content => notes
771 775 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
772 776 end
773 777
774 778 def test_bulk_edit
775 779 @request.session[:user_id] = 2
776 780 # update issues priority
777 781 post :bulk_edit, :ids => [1, 2], :priority_id => 7,
778 782 :assigned_to_id => '',
779 783 :custom_field_values => {'2' => ''},
780 784 :notes => 'Bulk editing'
781 785 assert_response 302
782 786 # check that the issues were updated
783 787 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
784 788
785 789 issue = Issue.find(1)
786 790 journal = issue.journals.find(:first, :order => 'created_on DESC')
787 791 assert_equal '125', issue.custom_value_for(2).value
788 792 assert_equal 'Bulk editing', journal.notes
789 793 assert_equal 1, journal.details.size
790 794 end
791 795
792 796 def test_bulk_edit_custom_field
793 797 @request.session[:user_id] = 2
794 798 # update issues priority
795 799 post :bulk_edit, :ids => [1, 2], :priority_id => '',
796 800 :assigned_to_id => '',
797 801 :custom_field_values => {'2' => '777'},
798 802 :notes => 'Bulk editing custom field'
799 803 assert_response 302
800 804
801 805 issue = Issue.find(1)
802 806 journal = issue.journals.find(:first, :order => 'created_on DESC')
803 807 assert_equal '777', issue.custom_value_for(2).value
804 808 assert_equal 1, journal.details.size
805 809 assert_equal '125', journal.details.first.old_value
806 810 assert_equal '777', journal.details.first.value
807 811 end
808 812
809 813 def test_bulk_unassign
810 814 assert_not_nil Issue.find(2).assigned_to
811 815 @request.session[:user_id] = 2
812 816 # unassign issues
813 817 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk unassigning', :assigned_to_id => 'none'
814 818 assert_response 302
815 819 # check that the issues were updated
816 820 assert_nil Issue.find(2).assigned_to
817 821 end
818 822
819 823 def test_move_routing
820 824 assert_routing(
821 825 {:method => :get, :path => '/issues/1/move'},
822 826 :controller => 'issues', :action => 'move', :id => '1'
823 827 )
824 828 assert_recognizes(
825 829 {:controller => 'issues', :action => 'move', :id => '1'},
826 830 {:method => :post, :path => '/issues/1/move'}
827 831 )
828 832 end
829 833
830 834 def test_move_one_issue_to_another_project
831 835 @request.session[:user_id] = 1
832 836 post :move, :id => 1, :new_project_id => 2
833 837 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
834 838 assert_equal 2, Issue.find(1).project_id
835 839 end
836 840
837 841 def test_bulk_move_to_another_project
838 842 @request.session[:user_id] = 1
839 843 post :move, :ids => [1, 2], :new_project_id => 2
840 844 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
841 845 # Issues moved to project 2
842 846 assert_equal 2, Issue.find(1).project_id
843 847 assert_equal 2, Issue.find(2).project_id
844 848 # No tracker change
845 849 assert_equal 1, Issue.find(1).tracker_id
846 850 assert_equal 2, Issue.find(2).tracker_id
847 851 end
848 852
849 853 def test_bulk_move_to_another_tracker
850 854 @request.session[:user_id] = 1
851 855 post :move, :ids => [1, 2], :new_tracker_id => 2
852 856 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
853 857 assert_equal 2, Issue.find(1).tracker_id
854 858 assert_equal 2, Issue.find(2).tracker_id
855 859 end
856 860
857 861 def test_bulk_copy_to_another_project
858 862 @request.session[:user_id] = 1
859 863 assert_difference 'Issue.count', 2 do
860 864 assert_no_difference 'Project.find(1).issues.count' do
861 865 post :move, :ids => [1, 2], :new_project_id => 2, :copy_options => {:copy => '1'}
862 866 end
863 867 end
864 868 assert_redirected_to 'projects/ecookbook/issues'
865 869 end
866 870
867 871 def test_context_menu_one_issue
868 872 @request.session[:user_id] = 2
869 873 get :context_menu, :ids => [1]
870 874 assert_response :success
871 875 assert_template 'context_menu'
872 876 assert_tag :tag => 'a', :content => 'Edit',
873 877 :attributes => { :href => '/issues/1/edit',
874 878 :class => 'icon-edit' }
875 879 assert_tag :tag => 'a', :content => 'Closed',
876 880 :attributes => { :href => '/issues/1/edit?issue%5Bstatus_id%5D=5',
877 881 :class => '' }
878 882 assert_tag :tag => 'a', :content => 'Immediate',
879 883 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;priority_id=8',
880 884 :class => '' }
881 885 assert_tag :tag => 'a', :content => 'Dave Lopper',
882 886 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1',
883 887 :class => '' }
884 888 assert_tag :tag => 'a', :content => 'Copy',
885 889 :attributes => { :href => '/projects/ecookbook/issues/1/copy',
886 890 :class => 'icon-copy' }
887 891 assert_tag :tag => 'a', :content => 'Move',
888 892 :attributes => { :href => '/issues/move?ids%5B%5D=1',
889 893 :class => 'icon-move' }
890 894 assert_tag :tag => 'a', :content => 'Delete',
891 895 :attributes => { :href => '/issues/destroy?ids%5B%5D=1',
892 896 :class => 'icon-del' }
893 897 end
894 898
895 899 def test_context_menu_one_issue_by_anonymous
896 900 get :context_menu, :ids => [1]
897 901 assert_response :success
898 902 assert_template 'context_menu'
899 903 assert_tag :tag => 'a', :content => 'Delete',
900 904 :attributes => { :href => '#',
901 905 :class => 'icon-del disabled' }
902 906 end
903 907
904 908 def test_context_menu_multiple_issues_of_same_project
905 909 @request.session[:user_id] = 2
906 910 get :context_menu, :ids => [1, 2]
907 911 assert_response :success
908 912 assert_template 'context_menu'
909 913 assert_tag :tag => 'a', :content => 'Edit',
910 914 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
911 915 :class => 'icon-edit' }
912 916 assert_tag :tag => 'a', :content => 'Immediate',
913 917 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2&amp;priority_id=8',
914 918 :class => '' }
915 919 assert_tag :tag => 'a', :content => 'Dave Lopper',
916 920 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
917 921 :class => '' }
918 922 assert_tag :tag => 'a', :content => 'Move',
919 923 :attributes => { :href => '/issues/move?ids%5B%5D=1&amp;ids%5B%5D=2',
920 924 :class => 'icon-move' }
921 925 assert_tag :tag => 'a', :content => 'Delete',
922 926 :attributes => { :href => '/issues/destroy?ids%5B%5D=1&amp;ids%5B%5D=2',
923 927 :class => 'icon-del' }
924 928 end
925 929
926 930 def test_context_menu_multiple_issues_of_different_project
927 931 @request.session[:user_id] = 2
928 932 get :context_menu, :ids => [1, 2, 4]
929 933 assert_response :success
930 934 assert_template 'context_menu'
931 935 assert_tag :tag => 'a', :content => 'Delete',
932 936 :attributes => { :href => '#',
933 937 :class => 'icon-del disabled' }
934 938 end
935 939
936 940 def test_destroy_routing
937 941 assert_recognizes( #TODO: use DELETE on issue URI (need to change forms)
938 942 {:controller => 'issues', :action => 'destroy', :id => '1'},
939 943 {:method => :post, :path => '/issues/1/destroy'}
940 944 )
941 945 end
942 946
943 947 def test_destroy_issue_with_no_time_entries
944 948 assert_nil TimeEntry.find_by_issue_id(2)
945 949 @request.session[:user_id] = 2
946 950 post :destroy, :id => 2
947 951 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
948 952 assert_nil Issue.find_by_id(2)
949 953 end
950 954
951 955 def test_destroy_issues_with_time_entries
952 956 @request.session[:user_id] = 2
953 957 post :destroy, :ids => [1, 3]
954 958 assert_response :success
955 959 assert_template 'destroy'
956 960 assert_not_nil assigns(:hours)
957 961 assert Issue.find_by_id(1) && Issue.find_by_id(3)
958 962 end
959 963
960 964 def test_destroy_issues_and_destroy_time_entries
961 965 @request.session[:user_id] = 2
962 966 post :destroy, :ids => [1, 3], :todo => 'destroy'
963 967 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
964 968 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
965 969 assert_nil TimeEntry.find_by_id([1, 2])
966 970 end
967 971
968 972 def test_destroy_issues_and_assign_time_entries_to_project
969 973 @request.session[:user_id] = 2
970 974 post :destroy, :ids => [1, 3], :todo => 'nullify'
971 975 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
972 976 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
973 977 assert_nil TimeEntry.find(1).issue_id
974 978 assert_nil TimeEntry.find(2).issue_id
975 979 end
976 980
977 981 def test_destroy_issues_and_reassign_time_entries_to_another_issue
978 982 @request.session[:user_id] = 2
979 983 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
980 984 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
981 985 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
982 986 assert_equal 2, TimeEntry.find(1).issue_id
983 987 assert_equal 2, TimeEntry.find(2).issue_id
984 988 end
985 989 end
General Comments 0
You need to be logged in to leave comments. Login now