##// END OF EJS Templates
Wrap text custom fields in the issue list (#8064)....
Jean-Philippe Lang -
r5212:5823481d6ef0
parent child
Show More
@@ -1,665 +1,673
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 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, :groupable, :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.groupable = options[:groupable] || false
26 26 if groupable == true
27 27 self.groupable = name.to_s
28 28 end
29 29 self.default_order = options[:default_order]
30 30 @caption_key = options[:caption] || "field_#{name}"
31 31 end
32 32
33 33 def caption
34 34 l(@caption_key)
35 35 end
36 36
37 37 # Returns true if the column is sortable, otherwise false
38 38 def sortable?
39 39 !sortable.nil?
40 40 end
41 41
42 42 def value(issue)
43 43 issue.send name
44 44 end
45
46 def css_classes
47 name
48 end
45 49 end
46 50
47 51 class QueryCustomFieldColumn < QueryColumn
48 52
49 53 def initialize(custom_field)
50 54 self.name = "cf_#{custom_field.id}".to_sym
51 55 self.sortable = custom_field.order_statement || false
52 56 if %w(list date bool int).include?(custom_field.field_format)
53 57 self.groupable = custom_field.order_statement
54 58 end
55 59 self.groupable ||= false
56 60 @cf = custom_field
57 61 end
58 62
59 63 def caption
60 64 @cf.name
61 65 end
62 66
63 67 def custom_field
64 68 @cf
65 69 end
66 70
67 71 def value(issue)
68 72 cv = issue.custom_values.detect {|v| v.custom_field_id == @cf.id}
69 73 cv && @cf.cast_value(cv.value)
70 74 end
75
76 def css_classes
77 @css_classes ||= "#{name} #{@cf.field_format}"
78 end
71 79 end
72 80
73 81 class Query < ActiveRecord::Base
74 82 class StatementInvalid < ::ActiveRecord::StatementInvalid
75 83 end
76 84
77 85 belongs_to :project
78 86 belongs_to :user
79 87 serialize :filters
80 88 serialize :column_names
81 89 serialize :sort_criteria, Array
82 90
83 91 attr_protected :project_id, :user_id
84 92
85 93 validates_presence_of :name, :on => :save
86 94 validates_length_of :name, :maximum => 255
87 95
88 96 @@operators = { "=" => :label_equals,
89 97 "!" => :label_not_equals,
90 98 "o" => :label_open_issues,
91 99 "c" => :label_closed_issues,
92 100 "!*" => :label_none,
93 101 "*" => :label_all,
94 102 ">=" => :label_greater_or_equal,
95 103 "<=" => :label_less_or_equal,
96 104 "<t+" => :label_in_less_than,
97 105 ">t+" => :label_in_more_than,
98 106 "t+" => :label_in,
99 107 "t" => :label_today,
100 108 "w" => :label_this_week,
101 109 ">t-" => :label_less_than_ago,
102 110 "<t-" => :label_more_than_ago,
103 111 "t-" => :label_ago,
104 112 "~" => :label_contains,
105 113 "!~" => :label_not_contains }
106 114
107 115 cattr_reader :operators
108 116
109 117 @@operators_by_filter_type = { :list => [ "=", "!" ],
110 118 :list_status => [ "o", "=", "!", "c", "*" ],
111 119 :list_optional => [ "=", "!", "!*", "*" ],
112 120 :list_subprojects => [ "*", "!*", "=" ],
113 121 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
114 122 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
115 123 :string => [ "=", "~", "!", "!~" ],
116 124 :text => [ "~", "!~" ],
117 125 :integer => [ "=", ">=", "<=", "!*", "*" ] }
118 126
119 127 cattr_reader :operators_by_filter_type
120 128
121 129 @@available_columns = [
122 130 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
123 131 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
124 132 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
125 133 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
126 134 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
127 135 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
128 136 QueryColumn.new(:author),
129 137 QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true),
130 138 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
131 139 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
132 140 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
133 141 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
134 142 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
135 143 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
136 144 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
137 145 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
138 146 ]
139 147 cattr_reader :available_columns
140 148
141 149 def initialize(attributes = nil)
142 150 super attributes
143 151 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
144 152 end
145 153
146 154 def after_initialize
147 155 # Store the fact that project is nil (used in #editable_by?)
148 156 @is_for_all = project.nil?
149 157 end
150 158
151 159 def validate
152 160 filters.each_key do |field|
153 161 errors.add label_for(field), :blank unless
154 162 # filter requires one or more values
155 163 (values_for(field) and !values_for(field).first.blank?) or
156 164 # filter doesn't require any value
157 165 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
158 166 end if filters
159 167 end
160 168
161 169 def editable_by?(user)
162 170 return false unless user
163 171 # Admin can edit them all and regular users can edit their private queries
164 172 return true if user.admin? || (!is_public && self.user_id == user.id)
165 173 # Members can not edit public queries that are for all project (only admin is allowed to)
166 174 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
167 175 end
168 176
169 177 def available_filters
170 178 return @available_filters if @available_filters
171 179
172 180 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
173 181
174 182 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
175 183 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
176 184 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
177 185 "subject" => { :type => :text, :order => 8 },
178 186 "created_on" => { :type => :date_past, :order => 9 },
179 187 "updated_on" => { :type => :date_past, :order => 10 },
180 188 "start_date" => { :type => :date, :order => 11 },
181 189 "due_date" => { :type => :date, :order => 12 },
182 190 "estimated_hours" => { :type => :integer, :order => 13 },
183 191 "done_ratio" => { :type => :integer, :order => 14 }}
184 192
185 193 user_values = []
186 194 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
187 195 if project
188 196 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
189 197 else
190 198 all_projects = Project.visible.all
191 199 if all_projects.any?
192 200 # members of visible projects
193 201 user_values += User.active.find(:all, :conditions => ["#{User.table_name}.id IN (SELECT DISTINCT user_id FROM members WHERE project_id IN (?))", all_projects.collect(&:id)]).sort.collect{|s| [s.name, s.id.to_s] }
194 202
195 203 # project filter
196 204 project_values = []
197 205 Project.project_tree(all_projects) do |p, level|
198 206 prefix = (level > 0 ? ('--' * level + ' ') : '')
199 207 project_values << ["#{prefix}#{p.name}", p.id.to_s]
200 208 end
201 209 @available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values} unless project_values.empty?
202 210 end
203 211 end
204 212 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
205 213 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
206 214
207 215 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
208 216 @available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty?
209 217
210 218 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
211 219 @available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty?
212 220
213 221 if User.current.logged?
214 222 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
215 223 end
216 224
217 225 if project
218 226 # project specific filters
219 227 categories = @project.issue_categories.all
220 228 unless categories.empty?
221 229 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => categories.collect{|s| [s.name, s.id.to_s] } }
222 230 end
223 231 versions = @project.shared_versions.all
224 232 unless versions.empty?
225 233 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
226 234 end
227 235 unless @project.leaf?
228 236 subprojects = @project.descendants.visible.all
229 237 unless subprojects.empty?
230 238 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => subprojects.collect{|s| [s.name, s.id.to_s] } }
231 239 end
232 240 end
233 241 add_custom_fields_filters(@project.all_issue_custom_fields)
234 242 else
235 243 # global filters for cross project issue list
236 244 system_shared_versions = Version.visible.find_all_by_sharing('system')
237 245 unless system_shared_versions.empty?
238 246 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => system_shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
239 247 end
240 248 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
241 249 end
242 250 @available_filters
243 251 end
244 252
245 253 def add_filter(field, operator, values)
246 254 # values must be an array
247 255 return unless values and values.is_a? Array # and !values.first.empty?
248 256 # check if field is defined as an available filter
249 257 if available_filters.has_key? field
250 258 filter_options = available_filters[field]
251 259 # check if operator is allowed for that filter
252 260 #if @@operators_by_filter_type[filter_options[:type]].include? operator
253 261 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
254 262 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
255 263 #end
256 264 filters[field] = {:operator => operator, :values => values }
257 265 end
258 266 end
259 267
260 268 def add_short_filter(field, expression)
261 269 return unless expression
262 270 parms = expression.scan(/^(o|c|!\*|!|\*)?(.*)$/).first
263 271 add_filter field, (parms[0] || "="), [parms[1] || ""]
264 272 end
265 273
266 274 # Add multiple filters using +add_filter+
267 275 def add_filters(fields, operators, values)
268 276 if fields.is_a?(Array) && operators.is_a?(Hash) && values.is_a?(Hash)
269 277 fields.each do |field|
270 278 add_filter(field, operators[field], values[field])
271 279 end
272 280 end
273 281 end
274 282
275 283 def has_filter?(field)
276 284 filters and filters[field]
277 285 end
278 286
279 287 def operator_for(field)
280 288 has_filter?(field) ? filters[field][:operator] : nil
281 289 end
282 290
283 291 def values_for(field)
284 292 has_filter?(field) ? filters[field][:values] : nil
285 293 end
286 294
287 295 def label_for(field)
288 296 label = available_filters[field][:name] if available_filters.has_key?(field)
289 297 label ||= field.gsub(/\_id$/, "")
290 298 end
291 299
292 300 def available_columns
293 301 return @available_columns if @available_columns
294 302 @available_columns = Query.available_columns
295 303 @available_columns += (project ?
296 304 project.all_issue_custom_fields :
297 305 IssueCustomField.find(:all)
298 306 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
299 307 end
300 308
301 309 def self.available_columns=(v)
302 310 self.available_columns = (v)
303 311 end
304 312
305 313 def self.add_available_column(column)
306 314 self.available_columns << (column) if column.is_a?(QueryColumn)
307 315 end
308 316
309 317 # Returns an array of columns that can be used to group the results
310 318 def groupable_columns
311 319 available_columns.select {|c| c.groupable}
312 320 end
313 321
314 322 # Returns a Hash of columns and the key for sorting
315 323 def sortable_columns
316 324 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
317 325 h[column.name.to_s] = column.sortable
318 326 h
319 327 })
320 328 end
321 329
322 330 def columns
323 331 if has_default_columns?
324 332 available_columns.select do |c|
325 333 # Adds the project column by default for cross-project lists
326 334 Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?)
327 335 end
328 336 else
329 337 # preserve the column_names order
330 338 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
331 339 end
332 340 end
333 341
334 342 def column_names=(names)
335 343 if names
336 344 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
337 345 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
338 346 # Set column_names to nil if default columns
339 347 if names.map(&:to_s) == Setting.issue_list_default_columns
340 348 names = nil
341 349 end
342 350 end
343 351 write_attribute(:column_names, names)
344 352 end
345 353
346 354 def has_column?(column)
347 355 column_names && column_names.include?(column.name)
348 356 end
349 357
350 358 def has_default_columns?
351 359 column_names.nil? || column_names.empty?
352 360 end
353 361
354 362 def sort_criteria=(arg)
355 363 c = []
356 364 if arg.is_a?(Hash)
357 365 arg = arg.keys.sort.collect {|k| arg[k]}
358 366 end
359 367 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
360 368 write_attribute(:sort_criteria, c)
361 369 end
362 370
363 371 def sort_criteria
364 372 read_attribute(:sort_criteria) || []
365 373 end
366 374
367 375 def sort_criteria_key(arg)
368 376 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
369 377 end
370 378
371 379 def sort_criteria_order(arg)
372 380 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
373 381 end
374 382
375 383 # Returns the SQL sort order that should be prepended for grouping
376 384 def group_by_sort_order
377 385 if grouped? && (column = group_by_column)
378 386 column.sortable.is_a?(Array) ?
379 387 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
380 388 "#{column.sortable} #{column.default_order}"
381 389 end
382 390 end
383 391
384 392 # Returns true if the query is a grouped query
385 393 def grouped?
386 394 !group_by_column.nil?
387 395 end
388 396
389 397 def group_by_column
390 398 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
391 399 end
392 400
393 401 def group_by_statement
394 402 group_by_column.try(:groupable)
395 403 end
396 404
397 405 def project_statement
398 406 project_clauses = []
399 407 if project && !@project.descendants.active.empty?
400 408 ids = [project.id]
401 409 if has_filter?("subproject_id")
402 410 case operator_for("subproject_id")
403 411 when '='
404 412 # include the selected subprojects
405 413 ids += values_for("subproject_id").each(&:to_i)
406 414 when '!*'
407 415 # main project only
408 416 else
409 417 # all subprojects
410 418 ids += project.descendants.collect(&:id)
411 419 end
412 420 elsif Setting.display_subprojects_issues?
413 421 ids += project.descendants.collect(&:id)
414 422 end
415 423 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
416 424 elsif project
417 425 project_clauses << "#{Project.table_name}.id = %d" % project.id
418 426 end
419 427 project_clauses << Issue.visible_condition(User.current)
420 428 project_clauses.join(' AND ')
421 429 end
422 430
423 431 def statement
424 432 # filters clauses
425 433 filters_clauses = []
426 434 filters.each_key do |field|
427 435 next if field == "subproject_id"
428 436 v = values_for(field).clone
429 437 next unless v and !v.empty?
430 438 operator = operator_for(field)
431 439
432 440 # "me" value subsitution
433 441 if %w(assigned_to_id author_id watcher_id).include?(field)
434 442 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
435 443 end
436 444
437 445 sql = ''
438 446 if field =~ /^cf_(\d+)$/
439 447 # custom field
440 448 db_table = CustomValue.table_name
441 449 db_field = 'value'
442 450 is_custom_filter = true
443 451 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 "
444 452 sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')'
445 453 elsif field == 'watcher_id'
446 454 db_table = Watcher.table_name
447 455 db_field = 'user_id'
448 456 sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND "
449 457 sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
450 458 elsif field == "member_of_group" # named field
451 459 if operator == '*' # Any group
452 460 groups = Group.all
453 461 operator = '=' # Override the operator since we want to find by assigned_to
454 462 elsif operator == "!*"
455 463 groups = Group.all
456 464 operator = '!' # Override the operator since we want to find by assigned_to
457 465 else
458 466 groups = Group.find_all_by_id(v)
459 467 end
460 468 groups ||= []
461 469
462 470 members_of_groups = groups.inject([]) {|user_ids, group|
463 471 if group && group.user_ids.present?
464 472 user_ids << group.user_ids
465 473 end
466 474 user_ids.flatten.uniq.compact
467 475 }.sort.collect(&:to_s)
468 476
469 477 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
470 478
471 479 elsif field == "assigned_to_role" # named field
472 480 if operator == "*" # Any Role
473 481 roles = Role.givable
474 482 operator = '=' # Override the operator since we want to find by assigned_to
475 483 elsif operator == "!*" # No role
476 484 roles = Role.givable
477 485 operator = '!' # Override the operator since we want to find by assigned_to
478 486 else
479 487 roles = Role.givable.find_all_by_id(v)
480 488 end
481 489 roles ||= []
482 490
483 491 members_of_roles = roles.inject([]) {|user_ids, role|
484 492 if role && role.members
485 493 user_ids << role.members.collect(&:user_id)
486 494 end
487 495 user_ids.flatten.uniq.compact
488 496 }.sort.collect(&:to_s)
489 497
490 498 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_roles, Issue.table_name, "assigned_to_id", false) + ')'
491 499 else
492 500 # regular field
493 501 db_table = Issue.table_name
494 502 db_field = field
495 503 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
496 504 end
497 505 filters_clauses << sql
498 506
499 507 end if filters and valid?
500 508
501 509 (filters_clauses << project_statement).join(' AND ')
502 510 end
503 511
504 512 # Returns the issue count
505 513 def issue_count
506 514 Issue.count(:include => [:status, :project], :conditions => statement)
507 515 rescue ::ActiveRecord::StatementInvalid => e
508 516 raise StatementInvalid.new(e.message)
509 517 end
510 518
511 519 # Returns the issue count by group or nil if query is not grouped
512 520 def issue_count_by_group
513 521 r = nil
514 522 if grouped?
515 523 begin
516 524 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
517 525 r = Issue.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
518 526 rescue ActiveRecord::RecordNotFound
519 527 r = {nil => issue_count}
520 528 end
521 529 c = group_by_column
522 530 if c.is_a?(QueryCustomFieldColumn)
523 531 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
524 532 end
525 533 end
526 534 r
527 535 rescue ::ActiveRecord::StatementInvalid => e
528 536 raise StatementInvalid.new(e.message)
529 537 end
530 538
531 539 # Returns the issues
532 540 # Valid options are :order, :offset, :limit, :include, :conditions
533 541 def issues(options={})
534 542 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
535 543 order_option = nil if order_option.blank?
536 544
537 545 Issue.find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
538 546 :conditions => Query.merge_conditions(statement, options[:conditions]),
539 547 :order => order_option,
540 548 :limit => options[:limit],
541 549 :offset => options[:offset]
542 550 rescue ::ActiveRecord::StatementInvalid => e
543 551 raise StatementInvalid.new(e.message)
544 552 end
545 553
546 554 # Returns the journals
547 555 # Valid options are :order, :offset, :limit
548 556 def journals(options={})
549 557 Journal.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
550 558 :conditions => statement,
551 559 :order => options[:order],
552 560 :limit => options[:limit],
553 561 :offset => options[:offset]
554 562 rescue ::ActiveRecord::StatementInvalid => e
555 563 raise StatementInvalid.new(e.message)
556 564 end
557 565
558 566 # Returns the versions
559 567 # Valid options are :conditions
560 568 def versions(options={})
561 569 Version.find :all, :include => :project,
562 570 :conditions => Query.merge_conditions(project_statement, options[:conditions])
563 571 rescue ::ActiveRecord::StatementInvalid => e
564 572 raise StatementInvalid.new(e.message)
565 573 end
566 574
567 575 private
568 576
569 577 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
570 578 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
571 579 sql = ''
572 580 case operator
573 581 when "="
574 582 if value.any?
575 583 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
576 584 else
577 585 # IN an empty set
578 586 sql = "1=0"
579 587 end
580 588 when "!"
581 589 if value.any?
582 590 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
583 591 else
584 592 # NOT IN an empty set
585 593 sql = "1=1"
586 594 end
587 595 when "!*"
588 596 sql = "#{db_table}.#{db_field} IS NULL"
589 597 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
590 598 when "*"
591 599 sql = "#{db_table}.#{db_field} IS NOT NULL"
592 600 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
593 601 when ">="
594 602 sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
595 603 when "<="
596 604 sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
597 605 when "o"
598 606 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
599 607 when "c"
600 608 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
601 609 when ">t-"
602 610 sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
603 611 when "<t-"
604 612 sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
605 613 when "t-"
606 614 sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
607 615 when ">t+"
608 616 sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
609 617 when "<t+"
610 618 sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
611 619 when "t+"
612 620 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
613 621 when "t"
614 622 sql = date_range_clause(db_table, db_field, 0, 0)
615 623 when "w"
616 624 from = l(:general_first_day_of_week) == '7' ?
617 625 # week starts on sunday
618 626 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
619 627 # week starts on monday (Rails default)
620 628 Time.now.at_beginning_of_week
621 629 sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
622 630 when "~"
623 631 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
624 632 when "!~"
625 633 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
626 634 end
627 635
628 636 return sql
629 637 end
630 638
631 639 def add_custom_fields_filters(custom_fields)
632 640 @available_filters ||= {}
633 641
634 642 custom_fields.select(&:is_filter?).each do |field|
635 643 case field.field_format
636 644 when "text"
637 645 options = { :type => :text, :order => 20 }
638 646 when "list"
639 647 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
640 648 when "date"
641 649 options = { :type => :date, :order => 20 }
642 650 when "bool"
643 651 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
644 652 when "user", "version"
645 653 next unless project
646 654 options = { :type => :list_optional, :values => field.possible_values_options(project), :order => 20}
647 655 else
648 656 options = { :type => :string, :order => 20 }
649 657 end
650 658 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
651 659 end
652 660 end
653 661
654 662 # Returns a SQL clause for a date or datetime field.
655 663 def date_range_clause(table, field, from, to)
656 664 s = []
657 665 if from
658 666 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
659 667 end
660 668 if to
661 669 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
662 670 end
663 671 s.join(' AND ')
664 672 end
665 673 end
@@ -1,37 +1,37
1 1 <% form_tag({}) do -%>
2 2 <%= hidden_field_tag 'back_url', url_for(params) %>
3 3 <div class="autoscroll">
4 4 <table class="list issues">
5 5 <thead><tr>
6 6 <th class="checkbox hide-when-print"><%= link_to image_tag('toggle_check.png'), {}, :onclick => 'toggleIssuesSelection(Element.up(this, "form")); return false;',
7 7 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
8 8 </th>
9 9 <%= sort_header_tag('id', :caption => '#', :default_order => 'desc') %>
10 10 <% query.columns.each do |column| %>
11 11 <%= column_header(column) %>
12 12 <% end %>
13 13 </tr></thead>
14 14 <% previous_group = false %>
15 15 <tbody>
16 16 <% issue_list(issues) do |issue, level| -%>
17 17 <% if @query.grouped? && (group = @query.group_by_column.value(issue)) != previous_group %>
18 18 <% reset_cycle %>
19 19 <tr class="group open">
20 20 <td colspan="<%= query.columns.size + 2 %>">
21 21 <span class="expander" onclick="toggleRowGroup(this); return false;">&nbsp;</span>
22 22 <%= group.blank? ? 'None' : column_content(@query.group_by_column, issue) %> <span class="count">(<%= @issue_count_by_group[group] %>)</span>
23 23 <%= link_to_function("#{l(:button_collapse_all)}/#{l(:button_expand_all)}", "toggleAllRowGroups(this)", :class => 'toggle-all') %>
24 24 </td>
25 25 </tr>
26 26 <% previous_group = group %>
27 27 <% end %>
28 28 <tr id="issue-<%= issue.id %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= issue.css_classes %> <%= level > 0 ? "idnt idnt-#{level}" : nil %>">
29 29 <td class="checkbox hide-when-print"><%= check_box_tag("ids[]", issue.id, false, :id => nil) %></td>
30 30 <td class="id"><%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %></td>
31 <% query.columns.each do |column| %><%= content_tag 'td', column_content(column, issue), :class => column.name %><% end %>
31 <% query.columns.each do |column| %><%= content_tag 'td', column_content(column, issue), :class => column.css_classes %><% end %>
32 32 </tr>
33 33 <% end -%>
34 34 </tbody>
35 35 </table>
36 36 </div>
37 37 <% end -%>
@@ -1,971 +1,972
1 1 html {overflow-y:scroll;}
2 2 body { font-family: Verdana, sans-serif; font-size: 12px; color:#484848; margin: 0; padding: 0; min-width: 900px; }
3 3
4 4 h1, h2, h3, h4 { font-family: "Trebuchet MS", Verdana, sans-serif;}
5 5 h1 {margin:0; padding:0; font-size: 24px;}
6 6 h2, .wiki h1 {font-size: 20px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
7 7 h3, .wiki h2 {font-size: 16px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
8 8 h4, .wiki h3 {font-size: 13px;padding: 2px 10px 1px 0px;margin-bottom: 5px; border-bottom: 1px dotted #bbbbbb; color: #444;}
9 9
10 10 /***** Layout *****/
11 11 #wrapper {background: white;}
12 12
13 13 #top-menu {background: #2C4056; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;}
14 14 #top-menu ul {margin: 0; padding: 0;}
15 15 #top-menu li {
16 16 float:left;
17 17 list-style-type:none;
18 18 margin: 0px 0px 0px 0px;
19 19 padding: 0px 0px 0px 0px;
20 20 white-space:nowrap;
21 21 }
22 22 #top-menu a {color: #fff; margin-right: 8px; font-weight: bold;}
23 23 #top-menu #loggedas { float: right; margin-right: 0.5em; color: #fff; }
24 24
25 25 #account {float:right;}
26 26
27 27 #header {height:5.3em;margin:0;background-color:#507AAA;color:#f8f8f8; padding: 4px 8px 0px 6px; position:relative;}
28 28 #header a {color:#f8f8f8;}
29 29 #header h1 a.ancestor { font-size: 80%; }
30 30 #quick-search {float:right;}
31 31
32 32 #main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px;}
33 33 #main-menu ul {margin: 0; padding: 0;}
34 34 #main-menu li {
35 35 float:left;
36 36 list-style-type:none;
37 37 margin: 0px 2px 0px 0px;
38 38 padding: 0px 0px 0px 0px;
39 39 white-space:nowrap;
40 40 }
41 41 #main-menu li a {
42 42 display: block;
43 43 color: #fff;
44 44 text-decoration: none;
45 45 font-weight: bold;
46 46 margin: 0;
47 47 padding: 4px 10px 4px 10px;
48 48 }
49 49 #main-menu li a:hover {background:#759FCF; color:#fff;}
50 50 #main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;}
51 51
52 52 #admin-menu ul {margin: 0; padding: 0;}
53 53 #admin-menu li {margin: 0; padding: 0 0 12px 0; list-style-type:none;}
54 54
55 55 #admin-menu a { background-position: 0% 40%; background-repeat: no-repeat; padding-left: 20px; padding-top: 2px; padding-bottom: 3px;}
56 56 #admin-menu a.projects { background-image: url(../images/projects.png); }
57 57 #admin-menu a.users { background-image: url(../images/user.png); }
58 58 #admin-menu a.groups { background-image: url(../images/group.png); }
59 59 #admin-menu a.roles { background-image: url(../images/database_key.png); }
60 60 #admin-menu a.trackers { background-image: url(../images/ticket.png); }
61 61 #admin-menu a.issue_statuses { background-image: url(../images/ticket_edit.png); }
62 62 #admin-menu a.workflows { background-image: url(../images/ticket_go.png); }
63 63 #admin-menu a.custom_fields { background-image: url(../images/textfield.png); }
64 64 #admin-menu a.enumerations { background-image: url(../images/text_list_bullets.png); }
65 65 #admin-menu a.settings { background-image: url(../images/changeset.png); }
66 66 #admin-menu a.plugins { background-image: url(../images/plugin.png); }
67 67 #admin-menu a.info { background-image: url(../images/help.png); }
68 68 #admin-menu a.server_authentication { background-image: url(../images/server_key.png); }
69 69
70 70 #main {background-color:#EEEEEE;}
71 71
72 72 #sidebar{ float: right; width: 22%; position: relative; z-index: 9; padding: 0; margin: 0;}
73 73 * html #sidebar{ width: 22%; }
74 74 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
75 75 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
76 76 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
77 77 #sidebar .contextual { margin-right: 1em; }
78 78
79 79 #content { width: 75%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; }
80 80 * html #content{ width: 75%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
81 81 html>body #content { min-height: 600px; }
82 82 * html body #content { height: 600px; } /* IE */
83 83
84 84 #main.nosidebar #sidebar{ display: none; }
85 85 #main.nosidebar #content{ width: auto; border-right: 0; }
86 86
87 87 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
88 88
89 89 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
90 90 #login-form table td {padding: 6px;}
91 91 #login-form label {font-weight: bold;}
92 92 #login-form input#username, #login-form input#password { width: 300px; }
93 93
94 94 input#openid_url { background: url(../images/openid-bg.gif) no-repeat; background-color: #fff; background-position: 0 50%; padding-left: 18px; }
95 95
96 96 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
97 97
98 98 /***** Links *****/
99 99 a, a:link, a:visited{ color: #2A5685; text-decoration: none; }
100 100 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
101 101 a img{ border: 0; }
102 102
103 103 a.issue.closed, a.issue.closed:link, a.issue.closed:visited { color: #999; text-decoration: line-through; }
104 104
105 105 /***** Tables *****/
106 106 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
107 107 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
108 108 table.list td { vertical-align: top; }
109 109 table.list td.id { width: 2%; text-align: center;}
110 110 table.list td.checkbox { width: 15px; padding: 2px 0 0 0; }
111 111 table.list td.checkbox input {padding:0px;}
112 112 table.list td.buttons { width: 15%; white-space:nowrap; text-align: right; }
113 113 table.list td.buttons a { padding-right: 0.6em; }
114 114 table.list caption { text-align: left; padding: 0.5em 0.5em 0.5em 0; }
115 115
116 116 tr.project td.name a { white-space:nowrap; }
117 117
118 118 tr.project.idnt td.name span {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
119 119 tr.project.idnt-1 td.name {padding-left: 0.5em;}
120 120 tr.project.idnt-2 td.name {padding-left: 2em;}
121 121 tr.project.idnt-3 td.name {padding-left: 3.5em;}
122 122 tr.project.idnt-4 td.name {padding-left: 5em;}
123 123 tr.project.idnt-5 td.name {padding-left: 6.5em;}
124 124 tr.project.idnt-6 td.name {padding-left: 8em;}
125 125 tr.project.idnt-7 td.name {padding-left: 9.5em;}
126 126 tr.project.idnt-8 td.name {padding-left: 11em;}
127 127 tr.project.idnt-9 td.name {padding-left: 12.5em;}
128 128
129 129 tr.issue { text-align: center; white-space: nowrap; }
130 tr.issue td.subject, tr.issue td.category, td.assigned_to { white-space: normal; }
130 tr.issue td.subject, tr.issue td.category, td.assigned_to, tr.issue td.string, tr.issue td.text { white-space: normal; }
131 131 tr.issue td.subject { text-align: left; }
132 132 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
133 tr.issue td.
133 134
134 135 tr.issue.idnt td.subject a {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
135 136 tr.issue.idnt-1 td.subject {padding-left: 0.5em;}
136 137 tr.issue.idnt-2 td.subject {padding-left: 2em;}
137 138 tr.issue.idnt-3 td.subject {padding-left: 3.5em;}
138 139 tr.issue.idnt-4 td.subject {padding-left: 5em;}
139 140 tr.issue.idnt-5 td.subject {padding-left: 6.5em;}
140 141 tr.issue.idnt-6 td.subject {padding-left: 8em;}
141 142 tr.issue.idnt-7 td.subject {padding-left: 9.5em;}
142 143 tr.issue.idnt-8 td.subject {padding-left: 11em;}
143 144 tr.issue.idnt-9 td.subject {padding-left: 12.5em;}
144 145
145 146 tr.entry { border: 1px solid #f8f8f8; }
146 147 tr.entry td { white-space: nowrap; }
147 148 tr.entry td.filename { width: 30%; }
148 149 tr.entry td.size { text-align: right; font-size: 90%; }
149 150 tr.entry td.revision, tr.entry td.author { text-align: center; }
150 151 tr.entry td.age { text-align: right; }
151 152 tr.entry.file td.filename a { margin-left: 16px; }
152 153
153 154 tr span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;}
154 155 tr.open span.expander {background-image: url(../images/bullet_toggle_minus.png);}
155 156
156 157 tr.changeset td.author { text-align: center; width: 15%; }
157 158 tr.changeset td.committed_on { text-align: center; width: 15%; }
158 159
159 160 table.files tr.file td { text-align: center; }
160 161 table.files tr.file td.filename { text-align: left; padding-left: 24px; }
161 162 table.files tr.file td.digest { font-size: 80%; }
162 163
163 164 table.members td.roles, table.memberships td.roles { width: 45%; }
164 165
165 166 tr.message { height: 2.6em; }
166 167 tr.message td.subject { padding-left: 20px; }
167 168 tr.message td.created_on { white-space: nowrap; }
168 169 tr.message td.last_message { font-size: 80%; white-space: nowrap; }
169 170 tr.message.locked td.subject { background: url(../images/locked.png) no-repeat 0 1px; }
170 171 tr.message.sticky td.subject { background: url(../images/bullet_go.png) no-repeat 0 1px; font-weight: bold; }
171 172
172 173 tr.version.closed, tr.version.closed a { color: #999; }
173 174 tr.version td.name { padding-left: 20px; }
174 175 tr.version.shared td.name { background: url(../images/link.png) no-repeat 0% 70%; }
175 176 tr.version td.date, tr.version td.status, tr.version td.sharing { text-align: center; white-space:nowrap; }
176 177
177 178 tr.user td { width:13%; }
178 179 tr.user td.email { width:18%; }
179 180 tr.user td { white-space: nowrap; }
180 181 tr.user.locked, tr.user.registered { color: #aaa; }
181 182 tr.user.locked a, tr.user.registered a { color: #aaa; }
182 183
183 184 tr.wiki-page-version td.updated_on, tr.wiki-page-version td.author {text-align:center;}
184 185
185 186 tr.time-entry { text-align: center; white-space: nowrap; }
186 187 tr.time-entry td.subject, tr.time-entry td.comments { text-align: left; white-space: normal; }
187 188 td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
188 189 td.hours .hours-dec { font-size: 0.9em; }
189 190
190 191 table.plugins td { vertical-align: middle; }
191 192 table.plugins td.configure { text-align: right; padding-right: 1em; }
192 193 table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px; }
193 194 table.plugins span.description { display: block; font-size: 0.9em; }
194 195 table.plugins span.url { display: block; font-size: 0.9em; }
195 196
196 197 table.list tbody tr.group td { padding: 0.8em 0 0.5em 0.3em; font-weight: bold; border-bottom: 1px solid #ccc; }
197 198 table.list tbody tr.group span.count { color: #aaa; font-size: 80%; }
198 199 tr.group a.toggle-all { color: #aaa; font-size: 80%; font-weight: normal; display:none;}
199 200 tr.group:hover a.toggle-all { display:inline;}
200 201 a.toggle-all:hover {text-decoration:none;}
201 202
202 203 table.list tbody tr:hover { background-color:#ffffdd; }
203 204 table.list tbody tr.group:hover { background-color:inherit; }
204 205 table td {padding:2px;}
205 206 table p {margin:0;}
206 207 .odd {background-color:#f6f7f8;}
207 208 .even {background-color: #fff;}
208 209
209 210 a.sort { padding-right: 16px; background-position: 100% 50%; background-repeat: no-repeat; }
210 211 a.sort.asc { background-image: url(../images/sort_asc.png); }
211 212 a.sort.desc { background-image: url(../images/sort_desc.png); }
212 213
213 214 table.attributes { width: 100% }
214 215 table.attributes th { vertical-align: top; text-align: left; }
215 216 table.attributes td { vertical-align: top; }
216 217
217 218 table.boards a.board, h3.comments { background: url(../images/comment.png) no-repeat 0% 50%; padding-left: 20px; }
218 219
219 220 td.center {text-align:center;}
220 221
221 222 h3.version { background: url(../images/package.png) no-repeat 0% 50%; padding-left: 20px; }
222 223
223 224 div.issues h3 { background: url(../images/ticket.png) no-repeat 0% 50%; padding-left: 20px; }
224 225 div.members h3 { background: url(../images/group.png) no-repeat 0% 50%; padding-left: 20px; }
225 226 div.news h3 { background: url(../images/news.png) no-repeat 0% 50%; padding-left: 20px; }
226 227 div.projects h3 { background: url(../images/projects.png) no-repeat 0% 50%; padding-left: 20px; }
227 228
228 229 #watchers ul {margin: 0; padding: 0;}
229 230 #watchers li {list-style-type:none;margin: 0px 2px 0px 0px; padding: 0px 0px 0px 0px;}
230 231 #watchers select {width: 95%; display: block;}
231 232 #watchers a.delete {opacity: 0.4;}
232 233 #watchers a.delete:hover {opacity: 1;}
233 234 #watchers img.gravatar {vertical-align: middle;margin: 0 4px 2px 0;}
234 235
235 236 .highlight { background-color: #FCFD8D;}
236 237 .highlight.token-1 { background-color: #faa;}
237 238 .highlight.token-2 { background-color: #afa;}
238 239 .highlight.token-3 { background-color: #aaf;}
239 240
240 241 .box{
241 242 padding:6px;
242 243 margin-bottom: 10px;
243 244 background-color:#f6f6f6;
244 245 color:#505050;
245 246 line-height:1.5em;
246 247 border: 1px solid #e4e4e4;
247 248 }
248 249
249 250 div.square {
250 251 border: 1px solid #999;
251 252 float: left;
252 253 margin: .3em .4em 0 .4em;
253 254 overflow: hidden;
254 255 width: .6em; height: .6em;
255 256 }
256 257 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;}
257 258 .contextual input, .contextual select {font-size:0.9em;}
258 259 .message .contextual { margin-top: 0; }
259 260
260 261 .splitcontentleft{float:left; width:49%;}
261 262 .splitcontentright{float:right; width:49%;}
262 263 form {display: inline;}
263 264 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
264 265 fieldset {border: 1px solid #e4e4e4; margin:0;}
265 266 legend {color: #484848;}
266 267 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
267 268 blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;}
268 269 blockquote blockquote { margin-left: 0;}
269 270 acronym { border-bottom: 1px dotted; cursor: help; }
270 271 textarea.wiki-edit { width: 99%; }
271 272 li p {margin-top: 0;}
272 273 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;}
273 274 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
274 275 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
275 276 p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; }
276 277
277 278 div.issue div.subject div div { padding-left: 16px; }
278 279 div.issue div.subject p {margin: 0; margin-bottom: 0.1em; font-size: 90%; color: #999;}
279 280 div.issue div.subject>div>p { margin-top: 0.5em; }
280 281 div.issue div.subject h3 {margin: 0; margin-bottom: 0.1em;}
281 282
282 283 #issue_tree table.issues, #relations table.issues { border: 0; }
283 284 #issue_tree td.checkbox, #relations td.checkbox {display:none;}
284 285 #relations td.buttons {padding:0;}
285 286
286 287 fieldset.collapsible { border-width: 1px 0 0 0; font-size: 0.9em; }
287 288 fieldset.collapsible legend { padding-left: 16px; background: url(../images/arrow_expanded.png) no-repeat 0% 40%; cursor:pointer; }
288 289 fieldset.collapsible.collapsed legend { background-image: url(../images/arrow_collapsed.png); }
289 290
290 291 fieldset#date-range p { margin: 2px 0 2px 0; }
291 292 fieldset#filters table { border-collapse: collapse; }
292 293 fieldset#filters table td { padding: 0; vertical-align: middle; }
293 294 fieldset#filters tr.filter { height: 2em; }
294 295 fieldset#filters td.add-filter { text-align: right; vertical-align: top; }
295 296 .buttons { font-size: 0.9em; margin-bottom: 1.4em; margin-top: 1em; }
296 297
297 298 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
298 299 div#issue-changesets div.changeset { padding: 4px;}
299 300 div#issue-changesets div.changeset { border-bottom: 1px solid #ddd; }
300 301 div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
301 302
302 303 div#activity dl, #search-results { margin-left: 2em; }
303 304 div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; }
304 305 div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
305 306 div#activity dt.me .time { border-bottom: 1px solid #999; }
306 307 div#activity dt .time { color: #777; font-size: 80%; }
307 308 div#activity dd .description, #search-results dd .description { font-style: italic; }
308 309 div#activity span.project:after, #search-results span.project:after { content: " -"; }
309 310 div#activity dd span.description, #search-results dd span.description { display:block; color: #808080; }
310 311
311 312 #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; }
312 313
313 314 div#search-results-counts {float:right;}
314 315 div#search-results-counts ul { margin-top: 0.5em; }
315 316 div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; }
316 317
317 318 dt.issue { background-image: url(../images/ticket.png); }
318 319 dt.issue-edit { background-image: url(../images/ticket_edit.png); }
319 320 dt.issue-closed { background-image: url(../images/ticket_checked.png); }
320 321 dt.issue-note { background-image: url(../images/ticket_note.png); }
321 322 dt.changeset { background-image: url(../images/changeset.png); }
322 323 dt.news { background-image: url(../images/news.png); }
323 324 dt.message { background-image: url(../images/message.png); }
324 325 dt.reply { background-image: url(../images/comments.png); }
325 326 dt.wiki-page { background-image: url(../images/wiki_edit.png); }
326 327 dt.attachment { background-image: url(../images/attachment.png); }
327 328 dt.document { background-image: url(../images/document.png); }
328 329 dt.project { background-image: url(../images/projects.png); }
329 330 dt.time-entry { background-image: url(../images/time.png); }
330 331
331 332 #search-results dt.issue.closed { background-image: url(../images/ticket_checked.png); }
332 333
333 334 div#roadmap .related-issues { margin-bottom: 1em; }
334 335 div#roadmap .related-issues td.checkbox { display: none; }
335 336 div#roadmap .wiki h1:first-child { display: none; }
336 337 div#roadmap .wiki h1 { font-size: 120%; }
337 338 div#roadmap .wiki h2 { font-size: 110%; }
338 339
339 340 div#version-summary { float:right; width:380px; margin-left: 16px; margin-bottom: 16px; background-color: #fff; }
340 341 div#version-summary fieldset { margin-bottom: 1em; }
341 342 div#version-summary .total-hours { text-align: right; }
342 343
343 344 table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; }
344 345 table#time-report tbody tr { font-style: italic; color: #777; }
345 346 table#time-report tbody tr.last-level { font-style: normal; color: #555; }
346 347 table#time-report tbody tr.total { font-style: normal; font-weight: bold; color: #555; background-color:#EEEEEE; }
347 348 table#time-report .hours-dec { font-size: 0.9em; }
348 349
349 350 form .attributes { margin-bottom: 8px; }
350 351 form .attributes p { padding-top: 1px; padding-bottom: 2px; }
351 352 form .attributes select { min-width: 50%; }
352 353
353 354 ul.projects { margin: 0; padding-left: 1em; }
354 355 ul.projects.root { margin: 0; padding: 0; }
355 356 ul.projects ul.projects { border-left: 3px solid #e0e0e0; }
356 357 ul.projects li.root { list-style-type:none; margin-bottom: 1em; }
357 358 ul.projects li.child { list-style-type:none; margin-top: 1em;}
358 359 ul.projects div.root a.project { font-family: "Trebuchet MS", Verdana, sans-serif; font-weight: bold; font-size: 16px; margin: 0 0 10px 0; }
359 360 .my-project { padding-left: 18px; background: url(../images/fav.png) no-repeat 0 50%; }
360 361
361 362 #tracker_project_ids ul { margin: 0; padding-left: 1em; }
362 363 #tracker_project_ids li { list-style-type:none; }
363 364
364 365 ul.properties {padding:0; font-size: 0.9em; color: #777;}
365 366 ul.properties li {list-style-type:none;}
366 367 ul.properties li span {font-style:italic;}
367 368
368 369 .total-hours { font-size: 110%; font-weight: bold; }
369 370 .total-hours span.hours-int { font-size: 120%; }
370 371
371 372 .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;}
372 373 #user_login, #user_firstname, #user_lastname, #user_mail, #my_account_form select, #user_form select { width: 90%; }
373 374
374 375 #workflow_copy_form select { width: 200px; }
375 376
376 377 textarea#custom_field_possible_values {width: 99%}
377 378
378 379 .pagination {font-size: 90%}
379 380 p.pagination {margin-top:8px;}
380 381
381 382 /***** Tabular forms ******/
382 383 .tabular p{
383 384 margin: 0;
384 385 padding: 5px 0 8px 0;
385 386 padding-left: 180px; /*width of left column containing the label elements*/
386 387 height: 1%;
387 388 clear:left;
388 389 }
389 390
390 391 html>body .tabular p {overflow:hidden;}
391 392
392 393 .tabular label{
393 394 font-weight: bold;
394 395 float: left;
395 396 text-align: right;
396 397 margin-left: -180px; /*width of left column*/
397 398 width: 175px; /*width of labels. Should be smaller than left column to create some right
398 399 margin*/
399 400 }
400 401
401 402 .tabular label.floating{
402 403 font-weight: normal;
403 404 margin-left: 0px;
404 405 text-align: left;
405 406 width: 270px;
406 407 }
407 408
408 409 .tabular label.block{
409 410 font-weight: normal;
410 411 margin-left: 0px !important;
411 412 text-align: left;
412 413 float: none;
413 414 display: block;
414 415 width: auto;
415 416 }
416 417
417 418 .tabular label.inline{
418 419 float:none;
419 420 margin-left: 5px !important;
420 421 width: auto;
421 422 }
422 423
423 424 input#time_entry_comments { width: 90%;}
424 425
425 426 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
426 427
427 428 .tabular.settings p{ padding-left: 300px; }
428 429 .tabular.settings label{ margin-left: -300px; width: 295px; }
429 430 .tabular.settings textarea { width: 99%; }
430 431
431 432 fieldset.settings label { display: block; }
432 433 fieldset#notified_events .parent { padding-left: 20px; }
433 434
434 435 .required {color: #bb0000;}
435 436 .summary {font-style: italic;}
436 437
437 438 #attachments_fields input[type=text] {margin-left: 8px; }
438 439
439 440 div.attachments { margin-top: 12px; }
440 441 div.attachments p { margin:4px 0 2px 0; }
441 442 div.attachments img { vertical-align: middle; }
442 443 div.attachments span.author { font-size: 0.9em; color: #888; }
443 444
444 445 p.other-formats { text-align: right; font-size:0.9em; color: #666; }
445 446 .other-formats span + span:before { content: "| "; }
446 447
447 448 a.atom { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
448 449
449 450 /* Project members tab */
450 451 div#tab-content-members .splitcontentleft, div#tab-content-memberships .splitcontentleft, div#tab-content-users .splitcontentleft { width: 64% }
451 452 div#tab-content-members .splitcontentright, div#tab-content-memberships .splitcontentright, div#tab-content-users .splitcontentright { width: 34% }
452 453 div#tab-content-members fieldset, div#tab-content-memberships fieldset, div#tab-content-users fieldset { padding:1em; margin-bottom: 1em; }
453 454 div#tab-content-members fieldset legend, div#tab-content-memberships fieldset legend, div#tab-content-users fieldset legend { font-weight: bold; }
454 455 div#tab-content-members fieldset label, div#tab-content-memberships fieldset label, div#tab-content-users fieldset label { display: block; }
455 456 div#tab-content-members fieldset div, div#tab-content-users fieldset div { max-height: 400px; overflow:auto; }
456 457
457 458 table.members td.group { padding-left: 20px; background: url(../images/group.png) no-repeat 0% 50%; }
458 459
459 460 input#principal_search, input#user_search {width:100%}
460 461
461 462 * html div#tab-content-members fieldset div { height: 450px; }
462 463
463 464 /***** Flash & error messages ****/
464 465 #errorExplanation, div.flash, .nodata, .warning {
465 466 padding: 4px 4px 4px 30px;
466 467 margin-bottom: 12px;
467 468 font-size: 1.1em;
468 469 border: 2px solid;
469 470 }
470 471
471 472 div.flash {margin-top: 8px;}
472 473
473 474 div.flash.error, #errorExplanation {
474 475 background: url(../images/exclamation.png) 8px 50% no-repeat;
475 476 background-color: #ffe3e3;
476 477 border-color: #dd0000;
477 478 color: #880000;
478 479 }
479 480
480 481 div.flash.notice {
481 482 background: url(../images/true.png) 8px 5px no-repeat;
482 483 background-color: #dfffdf;
483 484 border-color: #9fcf9f;
484 485 color: #005f00;
485 486 }
486 487
487 488 div.flash.warning {
488 489 background: url(../images/warning.png) 8px 5px no-repeat;
489 490 background-color: #FFEBC1;
490 491 border-color: #FDBF3B;
491 492 color: #A6750C;
492 493 text-align: left;
493 494 }
494 495
495 496 .nodata, .warning {
496 497 text-align: center;
497 498 background-color: #FFEBC1;
498 499 border-color: #FDBF3B;
499 500 color: #A6750C;
500 501 }
501 502
502 503 #errorExplanation ul { font-size: 0.9em;}
503 504 #errorExplanation h2, #errorExplanation p { display: none; }
504 505
505 506 /***** Ajax indicator ******/
506 507 #ajax-indicator {
507 508 position: absolute; /* fixed not supported by IE */
508 509 background-color:#eee;
509 510 border: 1px solid #bbb;
510 511 top:35%;
511 512 left:40%;
512 513 width:20%;
513 514 font-weight:bold;
514 515 text-align:center;
515 516 padding:0.6em;
516 517 z-index:100;
517 518 filter:alpha(opacity=50);
518 519 opacity: 0.5;
519 520 }
520 521
521 522 html>body #ajax-indicator { position: fixed; }
522 523
523 524 #ajax-indicator span {
524 525 background-position: 0% 40%;
525 526 background-repeat: no-repeat;
526 527 background-image: url(../images/loading.gif);
527 528 padding-left: 26px;
528 529 vertical-align: bottom;
529 530 }
530 531
531 532 /***** Calendar *****/
532 533 table.cal {border-collapse: collapse; width: 100%; margin: 0px 0 6px 0;border: 1px solid #d7d7d7;}
533 534 table.cal thead th {width: 14%; background-color:#EEEEEE; padding: 4px; }
534 535 table.cal thead th.week-number {width: auto;}
535 536 table.cal tbody tr {height: 100px;}
536 537 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
537 538 table.cal td.week-number { background-color:#EEEEEE; padding: 4px; border:none; font-size: 1em;}
538 539 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
539 540 table.cal td.odd p.day-num {color: #bbb;}
540 541 table.cal td.today {background:#ffffdd;}
541 542 table.cal td.today p.day-num {font-weight: bold;}
542 543 table.cal .starting a, p.cal.legend .starting {background: url(../images/bullet_go.png) no-repeat -1px -2px; padding-left:16px;}
543 544 table.cal .ending a, p.cal.legend .ending {background: url(../images/bullet_end.png) no-repeat -1px -2px; padding-left:16px;}
544 545 table.cal .starting.ending a, p.cal.legend .starting.ending {background: url(../images/bullet_diamond.png) no-repeat -1px -2px; padding-left:16px;}
545 546 p.cal.legend span {display:block;}
546 547
547 548 /***** Tooltips ******/
548 549 .tooltip{position:relative;z-index:24;}
549 550 .tooltip:hover{z-index:25;color:#000;}
550 551 .tooltip span.tip{display: none; text-align:left;}
551 552
552 553 div.tooltip:hover span.tip{
553 554 display:block;
554 555 position:absolute;
555 556 top:12px; left:24px; width:270px;
556 557 border:1px solid #555;
557 558 background-color:#fff;
558 559 padding: 4px;
559 560 font-size: 0.8em;
560 561 color:#505050;
561 562 }
562 563
563 564 /***** Progress bar *****/
564 565 table.progress {
565 566 border: 1px solid #D7D7D7;
566 567 border-collapse: collapse;
567 568 border-spacing: 0pt;
568 569 empty-cells: show;
569 570 text-align: center;
570 571 float:left;
571 572 margin: 1px 6px 1px 0px;
572 573 }
573 574
574 575 table.progress td { height: 0.9em; }
575 576 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
576 577 table.progress td.done { background: #DEF0DE none repeat scroll 0%; }
577 578 table.progress td.open { background: #FFF none repeat scroll 0%; }
578 579 p.pourcent {font-size: 80%;}
579 580 p.progress-info {clear: left; font-style: italic; font-size: 80%;}
580 581
581 582 /***** Tabs *****/
582 583 #content .tabs {height: 2.6em; margin-bottom:1.2em; position:relative; overflow:hidden;}
583 584 #content .tabs ul {margin:0; position:absolute; bottom:0; padding-left:1em; width: 2000px; border-bottom: 1px solid #bbbbbb;}
584 585 #content .tabs ul li {
585 586 float:left;
586 587 list-style-type:none;
587 588 white-space:nowrap;
588 589 margin-right:8px;
589 590 background:#fff;
590 591 position:relative;
591 592 margin-bottom:-1px;
592 593 }
593 594 #content .tabs ul li a{
594 595 display:block;
595 596 font-size: 0.9em;
596 597 text-decoration:none;
597 598 line-height:1.3em;
598 599 padding:4px 6px 4px 6px;
599 600 border: 1px solid #ccc;
600 601 border-bottom: 1px solid #bbbbbb;
601 602 background-color: #eeeeee;
602 603 color:#777;
603 604 font-weight:bold;
604 605 }
605 606
606 607 #content .tabs ul li a:hover {
607 608 background-color: #ffffdd;
608 609 text-decoration:none;
609 610 }
610 611
611 612 #content .tabs ul li a.selected {
612 613 background-color: #fff;
613 614 border: 1px solid #bbbbbb;
614 615 border-bottom: 1px solid #fff;
615 616 }
616 617
617 618 #content .tabs ul li a.selected:hover {
618 619 background-color: #fff;
619 620 }
620 621
621 622 div.tabs-buttons { position:absolute; right: 0; width: 48px; height: 24px; background: white; bottom: 0; border-bottom: 1px solid #bbbbbb; }
622 623
623 624 button.tab-left, button.tab-right {
624 625 font-size: 0.9em;
625 626 cursor: pointer;
626 627 height:24px;
627 628 border: 1px solid #ccc;
628 629 border-bottom: 1px solid #bbbbbb;
629 630 position:absolute;
630 631 padding:4px;
631 632 width: 20px;
632 633 bottom: -1px;
633 634 }
634 635
635 636 button.tab-left {
636 637 right: 20px;
637 638 background: #eeeeee url(../images/bullet_arrow_left.png) no-repeat 50% 50%;
638 639 }
639 640
640 641 button.tab-right {
641 642 right: 0;
642 643 background: #eeeeee url(../images/bullet_arrow_right.png) no-repeat 50% 50%;
643 644 }
644 645
645 646 /***** Auto-complete *****/
646 647 div.autocomplete {
647 648 position:absolute;
648 649 width:400px;
649 650 margin:0;
650 651 padding:0;
651 652 }
652 653 div.autocomplete ul {
653 654 list-style-type:none;
654 655 margin:0;
655 656 padding:0;
656 657 }
657 658 div.autocomplete ul li {
658 659 list-style-type:none;
659 660 display:block;
660 661 margin:-1px 0 0 0;
661 662 padding:2px;
662 663 cursor:pointer;
663 664 font-size: 90%;
664 665 border: 1px solid #ccc;
665 666 border-left: 1px solid #ccc;
666 667 border-right: 1px solid #ccc;
667 668 background-color:white;
668 669 }
669 670 div.autocomplete ul li.selected { background-color: #ffb;}
670 671 div.autocomplete ul li span.informal {
671 672 font-size: 80%;
672 673 color: #aaa;
673 674 }
674 675
675 676 #parent_issue_candidates ul li {width: 500px;}
676 677 #related_issue_candidates ul li {width: 500px;}
677 678
678 679 /***** Diff *****/
679 680 .diff_out { background: #fcc; }
680 681 .diff_out span { background: #faa; }
681 682 .diff_in { background: #cfc; }
682 683 .diff_in span { background: #afa; }
683 684
684 685 .text-diff {
685 686 padding: 1em;
686 687 background-color:#f6f6f6;
687 688 color:#505050;
688 689 border: 1px solid #e4e4e4;
689 690 }
690 691
691 692 /***** Wiki *****/
692 693 div.wiki table {
693 694 border: 1px solid #505050;
694 695 border-collapse: collapse;
695 696 margin-bottom: 1em;
696 697 }
697 698
698 699 div.wiki table, div.wiki td, div.wiki th {
699 700 border: 1px solid #bbb;
700 701 padding: 4px;
701 702 }
702 703
703 704 div.wiki .external {
704 705 background-position: 0% 60%;
705 706 background-repeat: no-repeat;
706 707 padding-left: 12px;
707 708 background-image: url(../images/external.png);
708 709 }
709 710
710 711 div.wiki a.new {
711 712 color: #b73535;
712 713 }
713 714
714 715 div.wiki pre {
715 716 margin: 1em 1em 1em 1.6em;
716 717 padding: 2px 2px 2px 0;
717 718 background-color: #fafafa;
718 719 border: 1px solid #dadada;
719 720 width:auto;
720 721 overflow-x: auto;
721 722 overflow-y: hidden;
722 723 }
723 724
724 725 div.wiki ul.toc {
725 726 background-color: #ffffdd;
726 727 border: 1px solid #e4e4e4;
727 728 padding: 4px;
728 729 line-height: 1.2em;
729 730 margin-bottom: 12px;
730 731 margin-right: 12px;
731 732 margin-left: 0;
732 733 display: table
733 734 }
734 735 * html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */
735 736
736 737 div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
737 738 div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
738 739 div.wiki ul.toc ul { margin: 0; padding: 0; }
739 740 div.wiki ul.toc li { list-style-type:none; margin: 0;}
740 741 div.wiki ul.toc li li { margin-left: 1.5em; }
741 742 div.wiki ul.toc li li li { font-size: 0.8em; }
742 743
743 744 div.wiki ul.toc a {
744 745 font-size: 0.9em;
745 746 font-weight: normal;
746 747 text-decoration: none;
747 748 color: #606060;
748 749 }
749 750 div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;}
750 751
751 752 a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; }
752 753 a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
753 754 h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; }
754 755
755 756 div.wiki img { vertical-align: middle; }
756 757
757 758 /***** My page layout *****/
758 759 .block-receiver {
759 760 border:1px dashed #c0c0c0;
760 761 margin-bottom: 20px;
761 762 padding: 15px 0 15px 0;
762 763 }
763 764
764 765 .mypage-box {
765 766 margin:0 0 20px 0;
766 767 color:#505050;
767 768 line-height:1.5em;
768 769 }
769 770
770 771 .handle {
771 772 cursor: move;
772 773 }
773 774
774 775 a.close-icon {
775 776 display:block;
776 777 margin-top:3px;
777 778 overflow:hidden;
778 779 width:12px;
779 780 height:12px;
780 781 background-repeat: no-repeat;
781 782 cursor:pointer;
782 783 background-image:url('../images/close.png');
783 784 }
784 785
785 786 a.close-icon:hover {
786 787 background-image:url('../images/close_hl.png');
787 788 }
788 789
789 790 /***** Gantt chart *****/
790 791 .gantt_hdr {
791 792 position:absolute;
792 793 top:0;
793 794 height:16px;
794 795 border-top: 1px solid #c0c0c0;
795 796 border-bottom: 1px solid #c0c0c0;
796 797 border-right: 1px solid #c0c0c0;
797 798 text-align: center;
798 799 overflow: hidden;
799 800 }
800 801
801 802 .gantt_subjects { font-size: 0.8em; }
802 803 .gantt_subjects div { line-height:16px;height:16px;overflow:hidden;white-space:nowrap;text-overflow: ellipsis; }
803 804
804 805 .task {
805 806 position: absolute;
806 807 height:8px;
807 808 font-size:0.8em;
808 809 color:#888;
809 810 padding:0;
810 811 margin:0;
811 812 line-height:16px;
812 813 white-space:nowrap;
813 814 }
814 815
815 816 .task.label {width:100%;}
816 817 .task.label.project, .task.label.version { font-weight: bold; }
817 818
818 819 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
819 820 .task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; }
820 821 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
821 822
822 823 .task_todo.parent { background: #888; border: 1px solid #888; height: 3px;}
823 824 .task_late.parent, .task_done.parent { height: 3px;}
824 825 .task.parent.marker.starting { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; left: 0px; top: -1px;}
825 826 .task.parent.marker.ending { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; right: 0px; top: -1px;}
826 827
827 828 .version.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
828 829 .version.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
829 830 .version.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
830 831 .version.marker { background-image:url(../images/version_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
831 832
832 833 .project.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
833 834 .project.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
834 835 .project.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
835 836 .project.marker { background-image:url(../images/project_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
836 837
837 838 .version-behind-schedule a, .issue-behind-schedule a {color: #f66914;}
838 839 .version-overdue a, .issue-overdue a, .project-overdue a {color: #f00;}
839 840
840 841 /***** Icons *****/
841 842 .icon {
842 843 background-position: 0% 50%;
843 844 background-repeat: no-repeat;
844 845 padding-left: 20px;
845 846 padding-top: 2px;
846 847 padding-bottom: 3px;
847 848 }
848 849
849 850 .icon-add { background-image: url(../images/add.png); }
850 851 .icon-edit { background-image: url(../images/edit.png); }
851 852 .icon-copy { background-image: url(../images/copy.png); }
852 853 .icon-duplicate { background-image: url(../images/duplicate.png); }
853 854 .icon-del { background-image: url(../images/delete.png); }
854 855 .icon-move { background-image: url(../images/move.png); }
855 856 .icon-save { background-image: url(../images/save.png); }
856 857 .icon-cancel { background-image: url(../images/cancel.png); }
857 858 .icon-multiple { background-image: url(../images/table_multiple.png); }
858 859 .icon-folder { background-image: url(../images/folder.png); }
859 860 .open .icon-folder { background-image: url(../images/folder_open.png); }
860 861 .icon-package { background-image: url(../images/package.png); }
861 862 .icon-user { background-image: url(../images/user.png); }
862 863 .icon-projects { background-image: url(../images/projects.png); }
863 864 .icon-help { background-image: url(../images/help.png); }
864 865 .icon-attachment { background-image: url(../images/attachment.png); }
865 866 .icon-history { background-image: url(../images/history.png); }
866 867 .icon-time { background-image: url(../images/time.png); }
867 868 .icon-time-add { background-image: url(../images/time_add.png); }
868 869 .icon-stats { background-image: url(../images/stats.png); }
869 870 .icon-warning { background-image: url(../images/warning.png); }
870 871 .icon-fav { background-image: url(../images/fav.png); }
871 872 .icon-fav-off { background-image: url(../images/fav_off.png); }
872 873 .icon-reload { background-image: url(../images/reload.png); }
873 874 .icon-lock { background-image: url(../images/locked.png); }
874 875 .icon-unlock { background-image: url(../images/unlock.png); }
875 876 .icon-checked { background-image: url(../images/true.png); }
876 877 .icon-details { background-image: url(../images/zoom_in.png); }
877 878 .icon-report { background-image: url(../images/report.png); }
878 879 .icon-comment { background-image: url(../images/comment.png); }
879 880 .icon-summary { background-image: url(../images/lightning.png); }
880 881 .icon-server-authentication { background-image: url(../images/server_key.png); }
881 882 .icon-issue { background-image: url(../images/ticket.png); }
882 883 .icon-zoom-in { background-image: url(../images/zoom_in.png); }
883 884 .icon-zoom-out { background-image: url(../images/zoom_out.png); }
884 885
885 886 .icon-file { background-image: url(../images/files/default.png); }
886 887 .icon-file.text-plain { background-image: url(../images/files/text.png); }
887 888 .icon-file.text-x-c { background-image: url(../images/files/c.png); }
888 889 .icon-file.text-x-csharp { background-image: url(../images/files/csharp.png); }
889 890 .icon-file.text-x-php { background-image: url(../images/files/php.png); }
890 891 .icon-file.text-x-ruby { background-image: url(../images/files/ruby.png); }
891 892 .icon-file.text-xml { background-image: url(../images/files/xml.png); }
892 893 .icon-file.image-gif { background-image: url(../images/files/image.png); }
893 894 .icon-file.image-jpeg { background-image: url(../images/files/image.png); }
894 895 .icon-file.image-png { background-image: url(../images/files/image.png); }
895 896 .icon-file.image-tiff { background-image: url(../images/files/image.png); }
896 897 .icon-file.application-pdf { background-image: url(../images/files/pdf.png); }
897 898 .icon-file.application-zip { background-image: url(../images/files/zip.png); }
898 899 .icon-file.application-x-gzip { background-image: url(../images/files/zip.png); }
899 900
900 901 img.gravatar {
901 902 padding: 2px;
902 903 border: solid 1px #d5d5d5;
903 904 background: #fff;
904 905 }
905 906
906 907 div.issue img.gravatar {
907 908 float: right;
908 909 margin: 0 0 0 1em;
909 910 padding: 5px;
910 911 }
911 912
912 913 div.issue table img.gravatar {
913 914 height: 14px;
914 915 width: 14px;
915 916 padding: 2px;
916 917 float: left;
917 918 margin: 0 0.5em 0 0;
918 919 }
919 920
920 921 h2 img.gravatar {
921 922 padding: 3px;
922 923 margin: -2px 4px -4px 0;
923 924 vertical-align: top;
924 925 }
925 926
926 927 h4 img.gravatar {
927 928 padding: 3px;
928 929 margin: -6px 0 -4px 0;
929 930 vertical-align: top;
930 931 }
931 932
932 933 td.username img.gravatar {
933 934 margin: 0 0.5em 0 0;
934 935 vertical-align: top;
935 936 }
936 937
937 938 #activity dt img.gravatar {
938 939 float: left;
939 940 margin: 0 1em 1em 0;
940 941 }
941 942
942 943 /* Used on 12px Gravatar img tags without the icon background */
943 944 .icon-gravatar {
944 945 float: left;
945 946 margin-right: 4px;
946 947 }
947 948
948 949 #activity dt,
949 950 .journal {
950 951 clear: left;
951 952 }
952 953
953 954 .journal-link {
954 955 float: right;
955 956 }
956 957
957 958 h2 img { vertical-align:middle; }
958 959
959 960 .hascontextmenu { cursor: context-menu; }
960 961
961 962 /***** Media print specific styles *****/
962 963 @media print {
963 964 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; }
964 965 #main { background: #fff; }
965 966 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;}
966 967 #wiki_add_attachment { display:none; }
967 968 .hide-when-print { display: none; }
968 969 .autoscroll {overflow-x: visible;}
969 970 table.list {margin-top:0.5em;}
970 971 table.list th, table.list td {border: 1px solid #aaa;}
971 972 }
@@ -1,1333 +1,1348
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.expand_path('../../test_helper', __FILE__)
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 < ActionController::TestCase
25 25 fixtures :projects,
26 26 :users,
27 27 :roles,
28 28 :members,
29 29 :member_roles,
30 30 :issues,
31 31 :issue_statuses,
32 32 :versions,
33 33 :trackers,
34 34 :projects_trackers,
35 35 :issue_categories,
36 36 :enabled_modules,
37 37 :enumerations,
38 38 :attachments,
39 39 :workflows,
40 40 :custom_fields,
41 41 :custom_values,
42 42 :custom_fields_projects,
43 43 :custom_fields_trackers,
44 44 :time_entries,
45 45 :journals,
46 46 :journal_details,
47 47 :queries
48 48
49 49 def setup
50 50 @controller = IssuesController.new
51 51 @request = ActionController::TestRequest.new
52 52 @response = ActionController::TestResponse.new
53 53 User.current = nil
54 54 end
55 55
56 56 def test_index
57 57 Setting.default_language = 'en'
58 58
59 59 get :index
60 60 assert_response :success
61 61 assert_template 'index.rhtml'
62 62 assert_not_nil assigns(:issues)
63 63 assert_nil assigns(:project)
64 64 assert_tag :tag => 'a', :content => /Can't print recipes/
65 65 assert_tag :tag => 'a', :content => /Subproject issue/
66 66 # private projects hidden
67 67 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
68 68 assert_no_tag :tag => 'a', :content => /Issue on project 2/
69 69 # project column
70 70 assert_tag :tag => 'th', :content => /Project/
71 71 end
72 72
73 73 def test_index_should_not_list_issues_when_module_disabled
74 74 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
75 75 get :index
76 76 assert_response :success
77 77 assert_template 'index.rhtml'
78 78 assert_not_nil assigns(:issues)
79 79 assert_nil assigns(:project)
80 80 assert_no_tag :tag => 'a', :content => /Can't print recipes/
81 81 assert_tag :tag => 'a', :content => /Subproject issue/
82 82 end
83 83
84 84 def test_index_should_not_list_issues_when_module_disabled
85 85 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
86 86 get :index
87 87 assert_response :success
88 88 assert_template 'index.rhtml'
89 89 assert_not_nil assigns(:issues)
90 90 assert_nil assigns(:project)
91 91 assert_no_tag :tag => 'a', :content => /Can't print recipes/
92 92 assert_tag :tag => 'a', :content => /Subproject issue/
93 93 end
94 94
95 95 def test_index_with_project
96 96 Setting.display_subprojects_issues = 0
97 97 get :index, :project_id => 1
98 98 assert_response :success
99 99 assert_template 'index.rhtml'
100 100 assert_not_nil assigns(:issues)
101 101 assert_tag :tag => 'a', :content => /Can't print recipes/
102 102 assert_no_tag :tag => 'a', :content => /Subproject issue/
103 103 end
104 104
105 105 def test_index_with_project_and_subprojects
106 106 Setting.display_subprojects_issues = 1
107 107 get :index, :project_id => 1
108 108 assert_response :success
109 109 assert_template 'index.rhtml'
110 110 assert_not_nil assigns(:issues)
111 111 assert_tag :tag => 'a', :content => /Can't print recipes/
112 112 assert_tag :tag => 'a', :content => /Subproject issue/
113 113 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
114 114 end
115 115
116 116 def test_index_with_project_and_subprojects_should_show_private_subprojects
117 117 @request.session[:user_id] = 2
118 118 Setting.display_subprojects_issues = 1
119 119 get :index, :project_id => 1
120 120 assert_response :success
121 121 assert_template 'index.rhtml'
122 122 assert_not_nil assigns(:issues)
123 123 assert_tag :tag => 'a', :content => /Can't print recipes/
124 124 assert_tag :tag => 'a', :content => /Subproject issue/
125 125 assert_tag :tag => 'a', :content => /Issue of a private subproject/
126 126 end
127 127
128 128 def test_index_with_project_and_default_filter
129 129 get :index, :project_id => 1, :set_filter => 1
130 130 assert_response :success
131 131 assert_template 'index.rhtml'
132 132 assert_not_nil assigns(:issues)
133 133
134 134 query = assigns(:query)
135 135 assert_not_nil query
136 136 # default filter
137 137 assert_equal({'status_id' => {:operator => 'o', :values => ['']}}, query.filters)
138 138 end
139 139
140 140 def test_index_with_project_and_filter
141 141 get :index, :project_id => 1, :set_filter => 1,
142 142 :f => ['tracker_id'],
143 143 :op => {'tracker_id' => '='},
144 144 :v => {'tracker_id' => ['1']}
145 145 assert_response :success
146 146 assert_template 'index.rhtml'
147 147 assert_not_nil assigns(:issues)
148 148
149 149 query = assigns(:query)
150 150 assert_not_nil query
151 151 assert_equal({'tracker_id' => {:operator => '=', :values => ['1']}}, query.filters)
152 152 end
153 153
154 154 def test_index_with_project_and_empty_filters
155 155 get :index, :project_id => 1, :set_filter => 1, :fields => ['']
156 156 assert_response :success
157 157 assert_template 'index.rhtml'
158 158 assert_not_nil assigns(:issues)
159 159
160 160 query = assigns(:query)
161 161 assert_not_nil query
162 162 # no filter
163 163 assert_equal({}, query.filters)
164 164 end
165 165
166 166 def test_index_with_query
167 167 get :index, :project_id => 1, :query_id => 5
168 168 assert_response :success
169 169 assert_template 'index.rhtml'
170 170 assert_not_nil assigns(:issues)
171 171 assert_nil assigns(:issue_count_by_group)
172 172 end
173 173
174 174 def test_index_with_query_grouped_by_tracker
175 175 get :index, :project_id => 1, :query_id => 6
176 176 assert_response :success
177 177 assert_template 'index.rhtml'
178 178 assert_not_nil assigns(:issues)
179 179 assert_not_nil assigns(:issue_count_by_group)
180 180 end
181 181
182 182 def test_index_with_query_grouped_by_list_custom_field
183 183 get :index, :project_id => 1, :query_id => 9
184 184 assert_response :success
185 185 assert_template 'index.rhtml'
186 186 assert_not_nil assigns(:issues)
187 187 assert_not_nil assigns(:issue_count_by_group)
188 188 end
189 189
190 190 def test_index_sort_by_field_not_included_in_columns
191 191 Setting.issue_list_default_columns = %w(subject author)
192 192 get :index, :sort => 'tracker'
193 193 end
194 194
195 195 def test_index_csv_with_project
196 196 Setting.default_language = 'en'
197 197
198 198 get :index, :format => 'csv'
199 199 assert_response :success
200 200 assert_not_nil assigns(:issues)
201 201 assert_equal 'text/csv', @response.content_type
202 202 assert @response.body.starts_with?("#,")
203 203
204 204 get :index, :project_id => 1, :format => 'csv'
205 205 assert_response :success
206 206 assert_not_nil assigns(:issues)
207 207 assert_equal 'text/csv', @response.content_type
208 208 end
209 209
210 210 def test_index_pdf
211 211 get :index, :format => 'pdf'
212 212 assert_response :success
213 213 assert_not_nil assigns(:issues)
214 214 assert_equal 'application/pdf', @response.content_type
215 215
216 216 get :index, :project_id => 1, :format => 'pdf'
217 217 assert_response :success
218 218 assert_not_nil assigns(:issues)
219 219 assert_equal 'application/pdf', @response.content_type
220 220
221 221 get :index, :project_id => 1, :query_id => 6, :format => 'pdf'
222 222 assert_response :success
223 223 assert_not_nil assigns(:issues)
224 224 assert_equal 'application/pdf', @response.content_type
225 225 end
226 226
227 227 def test_index_pdf_with_query_grouped_by_list_custom_field
228 228 get :index, :project_id => 1, :query_id => 9, :format => 'pdf'
229 229 assert_response :success
230 230 assert_not_nil assigns(:issues)
231 231 assert_not_nil assigns(:issue_count_by_group)
232 232 assert_equal 'application/pdf', @response.content_type
233 233 end
234 234
235 235 def test_index_sort
236 236 get :index, :sort => 'tracker,id:desc'
237 237 assert_response :success
238 238
239 239 sort_params = @request.session['issues_index_sort']
240 240 assert sort_params.is_a?(String)
241 241 assert_equal 'tracker,id:desc', sort_params
242 242
243 243 issues = assigns(:issues)
244 244 assert_not_nil issues
245 245 assert !issues.empty?
246 246 assert_equal issues.sort {|a,b| a.tracker == b.tracker ? b.id <=> a.id : a.tracker <=> b.tracker }.collect(&:id), issues.collect(&:id)
247 247 end
248 248
249 249 def test_index_with_columns
250 250 columns = ['tracker', 'subject', 'assigned_to']
251 251 get :index, :set_filter => 1, :c => columns
252 252 assert_response :success
253 253
254 254 # query should use specified columns
255 255 query = assigns(:query)
256 256 assert_kind_of Query, query
257 257 assert_equal columns, query.column_names.map(&:to_s)
258 258
259 259 # columns should be stored in session
260 260 assert_kind_of Hash, session[:query]
261 261 assert_kind_of Array, session[:query][:column_names]
262 262 assert_equal columns, session[:query][:column_names].map(&:to_s)
263 263 end
264
265 def test_index_with_custom_field_column
266 columns = %w(tracker subject cf_2)
267 get :index, :set_filter => 1, :c => columns
268 assert_response :success
269
270 # query should use specified columns
271 query = assigns(:query)
272 assert_kind_of Query, query
273 assert_equal columns, query.column_names.map(&:to_s)
274
275 assert_tag :td,
276 :attributes => {:class => 'cf_2 string'},
277 :ancestor => {:tag => 'table', :attributes => {:class => /issues/}}
278 end
264 279
265 280 def test_show_by_anonymous
266 281 get :show, :id => 1
267 282 assert_response :success
268 283 assert_template 'show.rhtml'
269 284 assert_not_nil assigns(:issue)
270 285 assert_equal Issue.find(1), assigns(:issue)
271 286
272 287 # anonymous role is allowed to add a note
273 288 assert_tag :tag => 'form',
274 289 :descendant => { :tag => 'fieldset',
275 290 :child => { :tag => 'legend',
276 291 :content => /Notes/ } }
277 292 end
278 293
279 294 def test_show_by_manager
280 295 @request.session[:user_id] = 2
281 296 get :show, :id => 1
282 297 assert_response :success
283 298
284 299 assert_tag :tag => 'a',
285 300 :content => /Quote/
286 301
287 302 assert_tag :tag => 'form',
288 303 :descendant => { :tag => 'fieldset',
289 304 :child => { :tag => 'legend',
290 305 :content => /Change properties/ } },
291 306 :descendant => { :tag => 'fieldset',
292 307 :child => { :tag => 'legend',
293 308 :content => /Log time/ } },
294 309 :descendant => { :tag => 'fieldset',
295 310 :child => { :tag => 'legend',
296 311 :content => /Notes/ } }
297 312 end
298 313
299 314 def test_show_should_deny_anonymous_access_without_permission
300 315 Role.anonymous.remove_permission!(:view_issues)
301 316 get :show, :id => 1
302 317 assert_response :redirect
303 318 end
304 319
305 320 def test_show_should_deny_non_member_access_without_permission
306 321 Role.non_member.remove_permission!(:view_issues)
307 322 @request.session[:user_id] = 9
308 323 get :show, :id => 1
309 324 assert_response 403
310 325 end
311 326
312 327 def test_show_should_deny_member_access_without_permission
313 328 Role.find(1).remove_permission!(:view_issues)
314 329 @request.session[:user_id] = 2
315 330 get :show, :id => 1
316 331 assert_response 403
317 332 end
318 333
319 334 def test_show_should_not_disclose_relations_to_invisible_issues
320 335 Setting.cross_project_issue_relations = '1'
321 336 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
322 337 # Relation to a private project issue
323 338 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates')
324 339
325 340 get :show, :id => 1
326 341 assert_response :success
327 342
328 343 assert_tag :div, :attributes => { :id => 'relations' },
329 344 :descendant => { :tag => 'a', :content => /#2$/ }
330 345 assert_no_tag :div, :attributes => { :id => 'relations' },
331 346 :descendant => { :tag => 'a', :content => /#4$/ }
332 347 end
333 348
334 349 def test_show_atom
335 350 get :show, :id => 2, :format => 'atom'
336 351 assert_response :success
337 352 assert_template 'journals/index.rxml'
338 353 # Inline image
339 354 assert_select 'content', :text => Regexp.new(Regexp.quote('http://test.host/attachments/download/10'))
340 355 end
341 356
342 357 def test_show_export_to_pdf
343 358 get :show, :id => 3, :format => 'pdf'
344 359 assert_response :success
345 360 assert_equal 'application/pdf', @response.content_type
346 361 assert @response.body.starts_with?('%PDF')
347 362 assert_not_nil assigns(:issue)
348 363 end
349 364
350 365 def test_get_new
351 366 @request.session[:user_id] = 2
352 367 get :new, :project_id => 1, :tracker_id => 1
353 368 assert_response :success
354 369 assert_template 'new'
355 370
356 371 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
357 372 :value => 'Default string' }
358 373 end
359 374
360 375 def test_get_new_without_tracker_id
361 376 @request.session[:user_id] = 2
362 377 get :new, :project_id => 1
363 378 assert_response :success
364 379 assert_template 'new'
365 380
366 381 issue = assigns(:issue)
367 382 assert_not_nil issue
368 383 assert_equal Project.find(1).trackers.first, issue.tracker
369 384 end
370 385
371 386 def test_get_new_with_no_default_status_should_display_an_error
372 387 @request.session[:user_id] = 2
373 388 IssueStatus.delete_all
374 389
375 390 get :new, :project_id => 1
376 391 assert_response 500
377 392 assert_error_tag :content => /No default issue/
378 393 end
379 394
380 395 def test_get_new_with_no_tracker_should_display_an_error
381 396 @request.session[:user_id] = 2
382 397 Tracker.delete_all
383 398
384 399 get :new, :project_id => 1
385 400 assert_response 500
386 401 assert_error_tag :content => /No tracker/
387 402 end
388 403
389 404 def test_update_new_form
390 405 @request.session[:user_id] = 2
391 406 xhr :post, :new, :project_id => 1,
392 407 :issue => {:tracker_id => 2,
393 408 :subject => 'This is the test_new issue',
394 409 :description => 'This is the description',
395 410 :priority_id => 5}
396 411 assert_response :success
397 412 assert_template 'attributes'
398 413
399 414 issue = assigns(:issue)
400 415 assert_kind_of Issue, issue
401 416 assert_equal 1, issue.project_id
402 417 assert_equal 2, issue.tracker_id
403 418 assert_equal 'This is the test_new issue', issue.subject
404 419 end
405 420
406 421 def test_post_create
407 422 @request.session[:user_id] = 2
408 423 assert_difference 'Issue.count' do
409 424 post :create, :project_id => 1,
410 425 :issue => {:tracker_id => 3,
411 426 :status_id => 2,
412 427 :subject => 'This is the test_new issue',
413 428 :description => 'This is the description',
414 429 :priority_id => 5,
415 430 :start_date => '2010-11-07',
416 431 :estimated_hours => '',
417 432 :custom_field_values => {'2' => 'Value for field 2'}}
418 433 end
419 434 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
420 435
421 436 issue = Issue.find_by_subject('This is the test_new issue')
422 437 assert_not_nil issue
423 438 assert_equal 2, issue.author_id
424 439 assert_equal 3, issue.tracker_id
425 440 assert_equal 2, issue.status_id
426 441 assert_equal Date.parse('2010-11-07'), issue.start_date
427 442 assert_nil issue.estimated_hours
428 443 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
429 444 assert_not_nil v
430 445 assert_equal 'Value for field 2', v.value
431 446 end
432 447
433 448 def test_post_create_without_start_date
434 449 @request.session[:user_id] = 2
435 450 assert_difference 'Issue.count' do
436 451 post :create, :project_id => 1,
437 452 :issue => {:tracker_id => 3,
438 453 :status_id => 2,
439 454 :subject => 'This is the test_new issue',
440 455 :description => 'This is the description',
441 456 :priority_id => 5,
442 457 :start_date => '',
443 458 :estimated_hours => '',
444 459 :custom_field_values => {'2' => 'Value for field 2'}}
445 460 end
446 461 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
447 462
448 463 issue = Issue.find_by_subject('This is the test_new issue')
449 464 assert_not_nil issue
450 465 assert_nil issue.start_date
451 466 end
452 467
453 468 def test_post_create_and_continue
454 469 @request.session[:user_id] = 2
455 470 post :create, :project_id => 1,
456 471 :issue => {:tracker_id => 3,
457 472 :subject => 'This is first issue',
458 473 :priority_id => 5},
459 474 :continue => ''
460 475 assert_redirected_to :controller => 'issues', :action => 'new', :project_id => 'ecookbook',
461 476 :issue => {:tracker_id => 3}
462 477 end
463 478
464 479 def test_post_create_without_custom_fields_param
465 480 @request.session[:user_id] = 2
466 481 assert_difference 'Issue.count' do
467 482 post :create, :project_id => 1,
468 483 :issue => {:tracker_id => 1,
469 484 :subject => 'This is the test_new issue',
470 485 :description => 'This is the description',
471 486 :priority_id => 5}
472 487 end
473 488 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
474 489 end
475 490
476 491 def test_post_create_with_required_custom_field_and_without_custom_fields_param
477 492 field = IssueCustomField.find_by_name('Database')
478 493 field.update_attribute(:is_required, true)
479 494
480 495 @request.session[:user_id] = 2
481 496 post :create, :project_id => 1,
482 497 :issue => {:tracker_id => 1,
483 498 :subject => 'This is the test_new issue',
484 499 :description => 'This is the description',
485 500 :priority_id => 5}
486 501 assert_response :success
487 502 assert_template 'new'
488 503 issue = assigns(:issue)
489 504 assert_not_nil issue
490 505 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
491 506 end
492 507
493 508 def test_post_create_with_watchers
494 509 @request.session[:user_id] = 2
495 510 ActionMailer::Base.deliveries.clear
496 511
497 512 assert_difference 'Watcher.count', 2 do
498 513 post :create, :project_id => 1,
499 514 :issue => {:tracker_id => 1,
500 515 :subject => 'This is a new issue with watchers',
501 516 :description => 'This is the description',
502 517 :priority_id => 5,
503 518 :watcher_user_ids => ['2', '3']}
504 519 end
505 520 issue = Issue.find_by_subject('This is a new issue with watchers')
506 521 assert_not_nil issue
507 522 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
508 523
509 524 # Watchers added
510 525 assert_equal [2, 3], issue.watcher_user_ids.sort
511 526 assert issue.watched_by?(User.find(3))
512 527 # Watchers notified
513 528 mail = ActionMailer::Base.deliveries.last
514 529 assert_kind_of TMail::Mail, mail
515 530 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
516 531 end
517 532
518 533 def test_post_create_subissue
519 534 @request.session[:user_id] = 2
520 535
521 536 assert_difference 'Issue.count' do
522 537 post :create, :project_id => 1,
523 538 :issue => {:tracker_id => 1,
524 539 :subject => 'This is a child issue',
525 540 :parent_issue_id => 2}
526 541 end
527 542 issue = Issue.find_by_subject('This is a child issue')
528 543 assert_not_nil issue
529 544 assert_equal Issue.find(2), issue.parent
530 545 end
531 546
532 547 def test_post_create_subissue_with_non_numeric_parent_id
533 548 @request.session[:user_id] = 2
534 549
535 550 assert_difference 'Issue.count' do
536 551 post :create, :project_id => 1,
537 552 :issue => {:tracker_id => 1,
538 553 :subject => 'This is a child issue',
539 554 :parent_issue_id => 'ABC'}
540 555 end
541 556 issue = Issue.find_by_subject('This is a child issue')
542 557 assert_not_nil issue
543 558 assert_nil issue.parent
544 559 end
545 560
546 561 def test_post_create_should_send_a_notification
547 562 ActionMailer::Base.deliveries.clear
548 563 @request.session[:user_id] = 2
549 564 assert_difference 'Issue.count' do
550 565 post :create, :project_id => 1,
551 566 :issue => {:tracker_id => 3,
552 567 :subject => 'This is the test_new issue',
553 568 :description => 'This is the description',
554 569 :priority_id => 5,
555 570 :estimated_hours => '',
556 571 :custom_field_values => {'2' => 'Value for field 2'}}
557 572 end
558 573 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
559 574
560 575 assert_equal 1, ActionMailer::Base.deliveries.size
561 576 end
562 577
563 578 def test_post_create_should_preserve_fields_values_on_validation_failure
564 579 @request.session[:user_id] = 2
565 580 post :create, :project_id => 1,
566 581 :issue => {:tracker_id => 1,
567 582 # empty subject
568 583 :subject => '',
569 584 :description => 'This is a description',
570 585 :priority_id => 6,
571 586 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
572 587 assert_response :success
573 588 assert_template 'new'
574 589
575 590 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
576 591 :content => 'This is a description'
577 592 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
578 593 :child => { :tag => 'option', :attributes => { :selected => 'selected',
579 594 :value => '6' },
580 595 :content => 'High' }
581 596 # Custom fields
582 597 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
583 598 :child => { :tag => 'option', :attributes => { :selected => 'selected',
584 599 :value => 'Oracle' },
585 600 :content => 'Oracle' }
586 601 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
587 602 :value => 'Value for field 2'}
588 603 end
589 604
590 605 def test_post_create_should_ignore_non_safe_attributes
591 606 @request.session[:user_id] = 2
592 607 assert_nothing_raised do
593 608 post :create, :project_id => 1, :issue => { :tracker => "A param can not be a Tracker" }
594 609 end
595 610 end
596 611
597 612 context "without workflow privilege" do
598 613 setup do
599 614 Workflow.delete_all(["role_id = ?", Role.anonymous.id])
600 615 Role.anonymous.add_permission! :add_issues, :add_issue_notes
601 616 end
602 617
603 618 context "#new" do
604 619 should "propose default status only" do
605 620 get :new, :project_id => 1
606 621 assert_response :success
607 622 assert_template 'new'
608 623 assert_tag :tag => 'select',
609 624 :attributes => {:name => 'issue[status_id]'},
610 625 :children => {:count => 1},
611 626 :child => {:tag => 'option', :attributes => {:value => IssueStatus.default.id.to_s}}
612 627 end
613 628
614 629 should "accept default status" do
615 630 assert_difference 'Issue.count' do
616 631 post :create, :project_id => 1,
617 632 :issue => {:tracker_id => 1,
618 633 :subject => 'This is an issue',
619 634 :status_id => 1}
620 635 end
621 636 issue = Issue.last(:order => 'id')
622 637 assert_equal IssueStatus.default, issue.status
623 638 end
624 639
625 640 should "ignore unauthorized status" do
626 641 assert_difference 'Issue.count' do
627 642 post :create, :project_id => 1,
628 643 :issue => {:tracker_id => 1,
629 644 :subject => 'This is an issue',
630 645 :status_id => 3}
631 646 end
632 647 issue = Issue.last(:order => 'id')
633 648 assert_equal IssueStatus.default, issue.status
634 649 end
635 650 end
636 651
637 652 context "#update" do
638 653 should "ignore status change" do
639 654 assert_difference 'Journal.count' do
640 655 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 3}
641 656 end
642 657 assert_equal 1, Issue.find(1).status_id
643 658 end
644 659
645 660 should "ignore attributes changes" do
646 661 assert_difference 'Journal.count' do
647 662 put :update, :id => 1, :notes => 'just trying', :issue => {:subject => 'changed', :assigned_to_id => 2}
648 663 end
649 664 issue = Issue.find(1)
650 665 assert_equal "Can't print recipes", issue.subject
651 666 assert_nil issue.assigned_to
652 667 end
653 668 end
654 669 end
655 670
656 671 context "with workflow privilege" do
657 672 setup do
658 673 Workflow.delete_all(["role_id = ?", Role.anonymous.id])
659 674 Workflow.create!(:role => Role.anonymous, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3)
660 675 Workflow.create!(:role => Role.anonymous, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4)
661 676 Role.anonymous.add_permission! :add_issues, :add_issue_notes
662 677 end
663 678
664 679 context "#update" do
665 680 should "accept authorized status" do
666 681 assert_difference 'Journal.count' do
667 682 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 3}
668 683 end
669 684 assert_equal 3, Issue.find(1).status_id
670 685 end
671 686
672 687 should "ignore unauthorized status" do
673 688 assert_difference 'Journal.count' do
674 689 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 2}
675 690 end
676 691 assert_equal 1, Issue.find(1).status_id
677 692 end
678 693
679 694 should "accept authorized attributes changes" do
680 695 assert_difference 'Journal.count' do
681 696 put :update, :id => 1, :notes => 'just trying', :issue => {:assigned_to_id => 2}
682 697 end
683 698 issue = Issue.find(1)
684 699 assert_equal 2, issue.assigned_to_id
685 700 end
686 701
687 702 should "ignore unauthorized attributes changes" do
688 703 assert_difference 'Journal.count' do
689 704 put :update, :id => 1, :notes => 'just trying', :issue => {:subject => 'changed'}
690 705 end
691 706 issue = Issue.find(1)
692 707 assert_equal "Can't print recipes", issue.subject
693 708 end
694 709 end
695 710
696 711 context "and :edit_issues permission" do
697 712 setup do
698 713 Role.anonymous.add_permission! :add_issues, :edit_issues
699 714 end
700 715
701 716 should "accept authorized status" do
702 717 assert_difference 'Journal.count' do
703 718 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 3}
704 719 end
705 720 assert_equal 3, Issue.find(1).status_id
706 721 end
707 722
708 723 should "ignore unauthorized status" do
709 724 assert_difference 'Journal.count' do
710 725 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 2}
711 726 end
712 727 assert_equal 1, Issue.find(1).status_id
713 728 end
714 729
715 730 should "accept authorized attributes changes" do
716 731 assert_difference 'Journal.count' do
717 732 put :update, :id => 1, :notes => 'just trying', :issue => {:subject => 'changed', :assigned_to_id => 2}
718 733 end
719 734 issue = Issue.find(1)
720 735 assert_equal "changed", issue.subject
721 736 assert_equal 2, issue.assigned_to_id
722 737 end
723 738 end
724 739 end
725 740
726 741 def test_copy_issue
727 742 @request.session[:user_id] = 2
728 743 get :new, :project_id => 1, :copy_from => 1
729 744 assert_template 'new'
730 745 assert_not_nil assigns(:issue)
731 746 orig = Issue.find(1)
732 747 assert_equal orig.subject, assigns(:issue).subject
733 748 end
734 749
735 750 def test_get_edit
736 751 @request.session[:user_id] = 2
737 752 get :edit, :id => 1
738 753 assert_response :success
739 754 assert_template 'edit'
740 755 assert_not_nil assigns(:issue)
741 756 assert_equal Issue.find(1), assigns(:issue)
742 757 end
743 758
744 759 def test_get_edit_with_params
745 760 @request.session[:user_id] = 2
746 761 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 },
747 762 :time_entry => { :hours => '2.5', :comments => 'test_get_edit_with_params', :activity_id => TimeEntryActivity.first.id }
748 763 assert_response :success
749 764 assert_template 'edit'
750 765
751 766 issue = assigns(:issue)
752 767 assert_not_nil issue
753 768
754 769 assert_equal 5, issue.status_id
755 770 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
756 771 :child => { :tag => 'option',
757 772 :content => 'Closed',
758 773 :attributes => { :selected => 'selected' } }
759 774
760 775 assert_equal 7, issue.priority_id
761 776 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
762 777 :child => { :tag => 'option',
763 778 :content => 'Urgent',
764 779 :attributes => { :selected => 'selected' } }
765 780
766 781 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => '2.5' }
767 782 assert_tag :select, :attributes => { :name => 'time_entry[activity_id]' },
768 783 :child => { :tag => 'option',
769 784 :attributes => { :selected => 'selected', :value => TimeEntryActivity.first.id } }
770 785 assert_tag :input, :attributes => { :name => 'time_entry[comments]', :value => 'test_get_edit_with_params' }
771 786 end
772 787
773 788 def test_update_edit_form
774 789 @request.session[:user_id] = 2
775 790 xhr :post, :new, :project_id => 1,
776 791 :id => 1,
777 792 :issue => {:tracker_id => 2,
778 793 :subject => 'This is the test_new issue',
779 794 :description => 'This is the description',
780 795 :priority_id => 5}
781 796 assert_response :success
782 797 assert_template 'attributes'
783 798
784 799 issue = assigns(:issue)
785 800 assert_kind_of Issue, issue
786 801 assert_equal 1, issue.id
787 802 assert_equal 1, issue.project_id
788 803 assert_equal 2, issue.tracker_id
789 804 assert_equal 'This is the test_new issue', issue.subject
790 805 end
791 806
792 807 def test_update_using_invalid_http_verbs
793 808 @request.session[:user_id] = 2
794 809 subject = 'Updated by an invalid http verb'
795 810
796 811 get :update, :id => 1, :issue => {:subject => subject}
797 812 assert_not_equal subject, Issue.find(1).subject
798 813
799 814 post :update, :id => 1, :issue => {:subject => subject}
800 815 assert_not_equal subject, Issue.find(1).subject
801 816
802 817 delete :update, :id => 1, :issue => {:subject => subject}
803 818 assert_not_equal subject, Issue.find(1).subject
804 819 end
805 820
806 821 def test_put_update_without_custom_fields_param
807 822 @request.session[:user_id] = 2
808 823 ActionMailer::Base.deliveries.clear
809 824
810 825 issue = Issue.find(1)
811 826 assert_equal '125', issue.custom_value_for(2).value
812 827 old_subject = issue.subject
813 828 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
814 829
815 830 assert_difference('Journal.count') do
816 831 assert_difference('JournalDetail.count', 2) do
817 832 put :update, :id => 1, :issue => {:subject => new_subject,
818 833 :priority_id => '6',
819 834 :category_id => '1' # no change
820 835 }
821 836 end
822 837 end
823 838 assert_redirected_to :action => 'show', :id => '1'
824 839 issue.reload
825 840 assert_equal new_subject, issue.subject
826 841 # Make sure custom fields were not cleared
827 842 assert_equal '125', issue.custom_value_for(2).value
828 843
829 844 mail = ActionMailer::Base.deliveries.last
830 845 assert_kind_of TMail::Mail, mail
831 846 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
832 847 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
833 848 end
834 849
835 850 def test_put_update_with_custom_field_change
836 851 @request.session[:user_id] = 2
837 852 issue = Issue.find(1)
838 853 assert_equal '125', issue.custom_value_for(2).value
839 854
840 855 assert_difference('Journal.count') do
841 856 assert_difference('JournalDetail.count', 3) do
842 857 put :update, :id => 1, :issue => {:subject => 'Custom field change',
843 858 :priority_id => '6',
844 859 :category_id => '1', # no change
845 860 :custom_field_values => { '2' => 'New custom value' }
846 861 }
847 862 end
848 863 end
849 864 assert_redirected_to :action => 'show', :id => '1'
850 865 issue.reload
851 866 assert_equal 'New custom value', issue.custom_value_for(2).value
852 867
853 868 mail = ActionMailer::Base.deliveries.last
854 869 assert_kind_of TMail::Mail, mail
855 870 assert mail.body.include?("Searchable field changed from 125 to New custom value")
856 871 end
857 872
858 873 def test_put_update_with_status_and_assignee_change
859 874 issue = Issue.find(1)
860 875 assert_equal 1, issue.status_id
861 876 @request.session[:user_id] = 2
862 877 assert_difference('TimeEntry.count', 0) do
863 878 put :update,
864 879 :id => 1,
865 880 :issue => { :status_id => 2, :assigned_to_id => 3 },
866 881 :notes => 'Assigned to dlopper',
867 882 :time_entry => { :hours => '', :comments => '', :activity_id => TimeEntryActivity.first }
868 883 end
869 884 assert_redirected_to :action => 'show', :id => '1'
870 885 issue.reload
871 886 assert_equal 2, issue.status_id
872 887 j = Journal.find(:first, :order => 'id DESC')
873 888 assert_equal 'Assigned to dlopper', j.notes
874 889 assert_equal 2, j.details.size
875 890
876 891 mail = ActionMailer::Base.deliveries.last
877 892 assert mail.body.include?("Status changed from New to Assigned")
878 893 # subject should contain the new status
879 894 assert mail.subject.include?("(#{ IssueStatus.find(2).name })")
880 895 end
881 896
882 897 def test_put_update_with_note_only
883 898 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
884 899 # anonymous user
885 900 put :update,
886 901 :id => 1,
887 902 :notes => notes
888 903 assert_redirected_to :action => 'show', :id => '1'
889 904 j = Journal.find(:first, :order => 'id DESC')
890 905 assert_equal notes, j.notes
891 906 assert_equal 0, j.details.size
892 907 assert_equal User.anonymous, j.user
893 908
894 909 mail = ActionMailer::Base.deliveries.last
895 910 assert mail.body.include?(notes)
896 911 end
897 912
898 913 def test_put_update_with_note_and_spent_time
899 914 @request.session[:user_id] = 2
900 915 spent_hours_before = Issue.find(1).spent_hours
901 916 assert_difference('TimeEntry.count') do
902 917 put :update,
903 918 :id => 1,
904 919 :notes => '2.5 hours added',
905 920 :time_entry => { :hours => '2.5', :comments => 'test_put_update_with_note_and_spent_time', :activity_id => TimeEntryActivity.first.id }
906 921 end
907 922 assert_redirected_to :action => 'show', :id => '1'
908 923
909 924 issue = Issue.find(1)
910 925
911 926 j = Journal.find(:first, :order => 'id DESC')
912 927 assert_equal '2.5 hours added', j.notes
913 928 assert_equal 0, j.details.size
914 929
915 930 t = issue.time_entries.find_by_comments('test_put_update_with_note_and_spent_time')
916 931 assert_not_nil t
917 932 assert_equal 2.5, t.hours
918 933 assert_equal spent_hours_before + 2.5, issue.spent_hours
919 934 end
920 935
921 936 def test_put_update_with_attachment_only
922 937 set_tmp_attachments_directory
923 938
924 939 # Delete all fixtured journals, a race condition can occur causing the wrong
925 940 # journal to get fetched in the next find.
926 941 Journal.delete_all
927 942
928 943 # anonymous user
929 944 put :update,
930 945 :id => 1,
931 946 :notes => '',
932 947 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
933 948 assert_redirected_to :action => 'show', :id => '1'
934 949 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
935 950 assert j.notes.blank?
936 951 assert_equal 1, j.details.size
937 952 assert_equal 'testfile.txt', j.details.first.value
938 953 assert_equal User.anonymous, j.user
939 954
940 955 mail = ActionMailer::Base.deliveries.last
941 956 assert mail.body.include?('testfile.txt')
942 957 end
943 958
944 959 def test_put_update_with_attachment_that_fails_to_save
945 960 set_tmp_attachments_directory
946 961
947 962 # Delete all fixtured journals, a race condition can occur causing the wrong
948 963 # journal to get fetched in the next find.
949 964 Journal.delete_all
950 965
951 966 # Mock out the unsaved attachment
952 967 Attachment.any_instance.stubs(:create).returns(Attachment.new)
953 968
954 969 # anonymous user
955 970 put :update,
956 971 :id => 1,
957 972 :notes => '',
958 973 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
959 974 assert_redirected_to :action => 'show', :id => '1'
960 975 assert_equal '1 file(s) could not be saved.', flash[:warning]
961 976
962 977 end if Object.const_defined?(:Mocha)
963 978
964 979 def test_put_update_with_no_change
965 980 issue = Issue.find(1)
966 981 issue.journals.clear
967 982 ActionMailer::Base.deliveries.clear
968 983
969 984 put :update,
970 985 :id => 1,
971 986 :notes => ''
972 987 assert_redirected_to :action => 'show', :id => '1'
973 988
974 989 issue.reload
975 990 assert issue.journals.empty?
976 991 # No email should be sent
977 992 assert ActionMailer::Base.deliveries.empty?
978 993 end
979 994
980 995 def test_put_update_should_send_a_notification
981 996 @request.session[:user_id] = 2
982 997 ActionMailer::Base.deliveries.clear
983 998 issue = Issue.find(1)
984 999 old_subject = issue.subject
985 1000 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
986 1001
987 1002 put :update, :id => 1, :issue => {:subject => new_subject,
988 1003 :priority_id => '6',
989 1004 :category_id => '1' # no change
990 1005 }
991 1006 assert_equal 1, ActionMailer::Base.deliveries.size
992 1007 end
993 1008
994 1009 def test_put_update_with_invalid_spent_time_hours_only
995 1010 @request.session[:user_id] = 2
996 1011 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
997 1012
998 1013 assert_no_difference('Journal.count') do
999 1014 put :update,
1000 1015 :id => 1,
1001 1016 :notes => notes,
1002 1017 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
1003 1018 end
1004 1019 assert_response :success
1005 1020 assert_template 'edit'
1006 1021
1007 1022 assert_error_tag :descendant => {:content => /Activity can't be blank/}
1008 1023 assert_tag :textarea, :attributes => { :name => 'notes' }, :content => notes
1009 1024 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
1010 1025 end
1011 1026
1012 1027 def test_put_update_with_invalid_spent_time_comments_only
1013 1028 @request.session[:user_id] = 2
1014 1029 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
1015 1030
1016 1031 assert_no_difference('Journal.count') do
1017 1032 put :update,
1018 1033 :id => 1,
1019 1034 :notes => notes,
1020 1035 :time_entry => {"comments"=>"this is my comment", "activity_id"=>"", "hours"=>""}
1021 1036 end
1022 1037 assert_response :success
1023 1038 assert_template 'edit'
1024 1039
1025 1040 assert_error_tag :descendant => {:content => /Activity can't be blank/}
1026 1041 assert_error_tag :descendant => {:content => /Hours can't be blank/}
1027 1042 assert_tag :textarea, :attributes => { :name => 'notes' }, :content => notes
1028 1043 assert_tag :input, :attributes => { :name => 'time_entry[comments]', :value => "this is my comment" }
1029 1044 end
1030 1045
1031 1046 def test_put_update_should_allow_fixed_version_to_be_set_to_a_subproject
1032 1047 issue = Issue.find(2)
1033 1048 @request.session[:user_id] = 2
1034 1049
1035 1050 put :update,
1036 1051 :id => issue.id,
1037 1052 :issue => {
1038 1053 :fixed_version_id => 4
1039 1054 }
1040 1055
1041 1056 assert_response :redirect
1042 1057 issue.reload
1043 1058 assert_equal 4, issue.fixed_version_id
1044 1059 assert_not_equal issue.project_id, issue.fixed_version.project_id
1045 1060 end
1046 1061
1047 1062 def test_put_update_should_redirect_back_using_the_back_url_parameter
1048 1063 issue = Issue.find(2)
1049 1064 @request.session[:user_id] = 2
1050 1065
1051 1066 put :update,
1052 1067 :id => issue.id,
1053 1068 :issue => {
1054 1069 :fixed_version_id => 4
1055 1070 },
1056 1071 :back_url => '/issues'
1057 1072
1058 1073 assert_response :redirect
1059 1074 assert_redirected_to '/issues'
1060 1075 end
1061 1076
1062 1077 def test_put_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
1063 1078 issue = Issue.find(2)
1064 1079 @request.session[:user_id] = 2
1065 1080
1066 1081 put :update,
1067 1082 :id => issue.id,
1068 1083 :issue => {
1069 1084 :fixed_version_id => 4
1070 1085 },
1071 1086 :back_url => 'http://google.com'
1072 1087
1073 1088 assert_response :redirect
1074 1089 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue.id
1075 1090 end
1076 1091
1077 1092 def test_get_bulk_edit
1078 1093 @request.session[:user_id] = 2
1079 1094 get :bulk_edit, :ids => [1, 2]
1080 1095 assert_response :success
1081 1096 assert_template 'bulk_edit'
1082 1097
1083 1098 assert_tag :input, :attributes => {:name => 'issue[parent_issue_id]'}
1084 1099
1085 1100 # Project specific custom field, date type
1086 1101 field = CustomField.find(9)
1087 1102 assert !field.is_for_all?
1088 1103 assert_equal 'date', field.field_format
1089 1104 assert_tag :input, :attributes => {:name => 'issue[custom_field_values][9]'}
1090 1105
1091 1106 # System wide custom field
1092 1107 assert CustomField.find(1).is_for_all?
1093 1108 assert_tag :select, :attributes => {:name => 'issue[custom_field_values][1]'}
1094 1109 end
1095 1110
1096 1111 def test_get_bulk_edit_on_different_projects
1097 1112 @request.session[:user_id] = 2
1098 1113 get :bulk_edit, :ids => [1, 2, 6]
1099 1114 assert_response :success
1100 1115 assert_template 'bulk_edit'
1101 1116
1102 1117 # Can not set issues from different projects as children of an issue
1103 1118 assert_no_tag :input, :attributes => {:name => 'issue[parent_issue_id]'}
1104 1119
1105 1120 # Project specific custom field, date type
1106 1121 field = CustomField.find(9)
1107 1122 assert !field.is_for_all?
1108 1123 assert !field.project_ids.include?(Issue.find(6).project_id)
1109 1124 assert_no_tag :input, :attributes => {:name => 'issue[custom_field_values][9]'}
1110 1125 end
1111 1126
1112 1127 def test_bulk_update
1113 1128 @request.session[:user_id] = 2
1114 1129 # update issues priority
1115 1130 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing',
1116 1131 :issue => {:priority_id => 7,
1117 1132 :assigned_to_id => '',
1118 1133 :custom_field_values => {'2' => ''}}
1119 1134
1120 1135 assert_response 302
1121 1136 # check that the issues were updated
1122 1137 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
1123 1138
1124 1139 issue = Issue.find(1)
1125 1140 journal = issue.journals.find(:first, :order => 'created_on DESC')
1126 1141 assert_equal '125', issue.custom_value_for(2).value
1127 1142 assert_equal 'Bulk editing', journal.notes
1128 1143 assert_equal 1, journal.details.size
1129 1144 end
1130 1145
1131 1146 def test_bulk_update_on_different_projects
1132 1147 @request.session[:user_id] = 2
1133 1148 # update issues priority
1134 1149 post :bulk_update, :ids => [1, 2, 6], :notes => 'Bulk editing',
1135 1150 :issue => {:priority_id => 7,
1136 1151 :assigned_to_id => '',
1137 1152 :custom_field_values => {'2' => ''}}
1138 1153
1139 1154 assert_response 302
1140 1155 # check that the issues were updated
1141 1156 assert_equal [7, 7, 7], Issue.find([1,2,6]).map(&:priority_id)
1142 1157
1143 1158 issue = Issue.find(1)
1144 1159 journal = issue.journals.find(:first, :order => 'created_on DESC')
1145 1160 assert_equal '125', issue.custom_value_for(2).value
1146 1161 assert_equal 'Bulk editing', journal.notes
1147 1162 assert_equal 1, journal.details.size
1148 1163 end
1149 1164
1150 1165 def test_bulk_update_on_different_projects_without_rights
1151 1166 @request.session[:user_id] = 3
1152 1167 user = User.find(3)
1153 1168 action = { :controller => "issues", :action => "bulk_update" }
1154 1169 assert user.allowed_to?(action, Issue.find(1).project)
1155 1170 assert ! user.allowed_to?(action, Issue.find(6).project)
1156 1171 post :bulk_update, :ids => [1, 6], :notes => 'Bulk should fail',
1157 1172 :issue => {:priority_id => 7,
1158 1173 :assigned_to_id => '',
1159 1174 :custom_field_values => {'2' => ''}}
1160 1175 assert_response 403
1161 1176 assert_not_equal "Bulk should fail", Journal.last.notes
1162 1177 end
1163 1178
1164 1179 def test_bullk_update_should_send_a_notification
1165 1180 @request.session[:user_id] = 2
1166 1181 ActionMailer::Base.deliveries.clear
1167 1182 post(:bulk_update,
1168 1183 {
1169 1184 :ids => [1, 2],
1170 1185 :notes => 'Bulk editing',
1171 1186 :issue => {
1172 1187 :priority_id => 7,
1173 1188 :assigned_to_id => '',
1174 1189 :custom_field_values => {'2' => ''}
1175 1190 }
1176 1191 })
1177 1192
1178 1193 assert_response 302
1179 1194 assert_equal 2, ActionMailer::Base.deliveries.size
1180 1195 end
1181 1196
1182 1197 def test_bulk_update_status
1183 1198 @request.session[:user_id] = 2
1184 1199 # update issues priority
1185 1200 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing status',
1186 1201 :issue => {:priority_id => '',
1187 1202 :assigned_to_id => '',
1188 1203 :status_id => '5'}
1189 1204
1190 1205 assert_response 302
1191 1206 issue = Issue.find(1)
1192 1207 assert issue.closed?
1193 1208 end
1194 1209
1195 1210 def test_bulk_update_parent_id
1196 1211 @request.session[:user_id] = 2
1197 1212 post :bulk_update, :ids => [1, 3],
1198 1213 :notes => 'Bulk editing parent',
1199 1214 :issue => {:priority_id => '', :assigned_to_id => '', :status_id => '', :parent_issue_id => '2'}
1200 1215
1201 1216 assert_response 302
1202 1217 parent = Issue.find(2)
1203 1218 assert_equal parent.id, Issue.find(1).parent_id
1204 1219 assert_equal parent.id, Issue.find(3).parent_id
1205 1220 assert_equal [1, 3], parent.children.collect(&:id).sort
1206 1221 end
1207 1222
1208 1223 def test_bulk_update_custom_field
1209 1224 @request.session[:user_id] = 2
1210 1225 # update issues priority
1211 1226 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing custom field',
1212 1227 :issue => {:priority_id => '',
1213 1228 :assigned_to_id => '',
1214 1229 :custom_field_values => {'2' => '777'}}
1215 1230
1216 1231 assert_response 302
1217 1232
1218 1233 issue = Issue.find(1)
1219 1234 journal = issue.journals.find(:first, :order => 'created_on DESC')
1220 1235 assert_equal '777', issue.custom_value_for(2).value
1221 1236 assert_equal 1, journal.details.size
1222 1237 assert_equal '125', journal.details.first.old_value
1223 1238 assert_equal '777', journal.details.first.value
1224 1239 end
1225 1240
1226 1241 def test_bulk_update_unassign
1227 1242 assert_not_nil Issue.find(2).assigned_to
1228 1243 @request.session[:user_id] = 2
1229 1244 # unassign issues
1230 1245 post :bulk_update, :ids => [1, 2], :notes => 'Bulk unassigning', :issue => {:assigned_to_id => 'none'}
1231 1246 assert_response 302
1232 1247 # check that the issues were updated
1233 1248 assert_nil Issue.find(2).assigned_to
1234 1249 end
1235 1250
1236 1251 def test_post_bulk_update_should_allow_fixed_version_to_be_set_to_a_subproject
1237 1252 @request.session[:user_id] = 2
1238 1253
1239 1254 post :bulk_update, :ids => [1,2], :issue => {:fixed_version_id => 4}
1240 1255
1241 1256 assert_response :redirect
1242 1257 issues = Issue.find([1,2])
1243 1258 issues.each do |issue|
1244 1259 assert_equal 4, issue.fixed_version_id
1245 1260 assert_not_equal issue.project_id, issue.fixed_version.project_id
1246 1261 end
1247 1262 end
1248 1263
1249 1264 def test_post_bulk_update_should_redirect_back_using_the_back_url_parameter
1250 1265 @request.session[:user_id] = 2
1251 1266 post :bulk_update, :ids => [1,2], :back_url => '/issues'
1252 1267
1253 1268 assert_response :redirect
1254 1269 assert_redirected_to '/issues'
1255 1270 end
1256 1271
1257 1272 def test_post_bulk_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
1258 1273 @request.session[:user_id] = 2
1259 1274 post :bulk_update, :ids => [1,2], :back_url => 'http://google.com'
1260 1275
1261 1276 assert_response :redirect
1262 1277 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => Project.find(1).identifier
1263 1278 end
1264 1279
1265 1280 def test_destroy_issue_with_no_time_entries
1266 1281 assert_nil TimeEntry.find_by_issue_id(2)
1267 1282 @request.session[:user_id] = 2
1268 1283 post :destroy, :id => 2
1269 1284 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1270 1285 assert_nil Issue.find_by_id(2)
1271 1286 end
1272 1287
1273 1288 def test_destroy_issues_with_time_entries
1274 1289 @request.session[:user_id] = 2
1275 1290 post :destroy, :ids => [1, 3]
1276 1291 assert_response :success
1277 1292 assert_template 'destroy'
1278 1293 assert_not_nil assigns(:hours)
1279 1294 assert Issue.find_by_id(1) && Issue.find_by_id(3)
1280 1295 end
1281 1296
1282 1297 def test_destroy_issues_and_destroy_time_entries
1283 1298 @request.session[:user_id] = 2
1284 1299 post :destroy, :ids => [1, 3], :todo => 'destroy'
1285 1300 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1286 1301 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1287 1302 assert_nil TimeEntry.find_by_id([1, 2])
1288 1303 end
1289 1304
1290 1305 def test_destroy_issues_and_assign_time_entries_to_project
1291 1306 @request.session[:user_id] = 2
1292 1307 post :destroy, :ids => [1, 3], :todo => 'nullify'
1293 1308 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1294 1309 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1295 1310 assert_nil TimeEntry.find(1).issue_id
1296 1311 assert_nil TimeEntry.find(2).issue_id
1297 1312 end
1298 1313
1299 1314 def test_destroy_issues_and_reassign_time_entries_to_another_issue
1300 1315 @request.session[:user_id] = 2
1301 1316 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
1302 1317 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1303 1318 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1304 1319 assert_equal 2, TimeEntry.find(1).issue_id
1305 1320 assert_equal 2, TimeEntry.find(2).issue_id
1306 1321 end
1307 1322
1308 1323 def test_destroy_issues_from_different_projects
1309 1324 @request.session[:user_id] = 2
1310 1325 post :destroy, :ids => [1, 2, 6], :todo => 'destroy'
1311 1326 assert_redirected_to :controller => 'issues', :action => 'index'
1312 1327 assert !(Issue.find_by_id(1) || Issue.find_by_id(2) || Issue.find_by_id(6))
1313 1328 end
1314 1329
1315 1330 def test_destroy_parent_and_child_issues
1316 1331 parent = Issue.generate!(:project_id => 1, :tracker_id => 1)
1317 1332 child = Issue.generate!(:project_id => 1, :tracker_id => 1, :parent_issue_id => parent.id)
1318 1333 assert child.is_descendant_of?(parent.reload)
1319 1334
1320 1335 @request.session[:user_id] = 2
1321 1336 assert_difference 'Issue.count', -2 do
1322 1337 post :destroy, :ids => [parent.id, child.id], :todo => 'destroy'
1323 1338 end
1324 1339 assert_response 302
1325 1340 end
1326 1341
1327 1342 def test_default_search_scope
1328 1343 get :index
1329 1344 assert_tag :div, :attributes => {:id => 'quick-search'},
1330 1345 :child => {:tag => 'form',
1331 1346 :child => {:tag => 'input', :attributes => {:name => 'issues', :type => 'hidden', :value => '1'}}}
1332 1347 end
1333 1348 end
General Comments 0
You need to be logged in to leave comments. Login now