##// END OF EJS Templates
Don't prepend project name if the version is not shared....
Jean-Philippe Lang -
r12972:2e04614e218e
parent child
Show More
@@ -1,1360 +1,1360
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2014 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 require 'forwardable'
21 21 require 'cgi'
22 22
23 23 module ApplicationHelper
24 24 include Redmine::WikiFormatting::Macros::Definitions
25 25 include Redmine::I18n
26 26 include GravatarHelper::PublicMethods
27 27 include Redmine::Pagination::Helper
28 28
29 29 extend Forwardable
30 30 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
31 31
32 32 # Return true if user is authorized for controller/action, otherwise false
33 33 def authorize_for(controller, action)
34 34 User.current.allowed_to?({:controller => controller, :action => action}, @project)
35 35 end
36 36
37 37 # Display a link if user is authorized
38 38 #
39 39 # @param [String] name Anchor text (passed to link_to)
40 40 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
41 41 # @param [optional, Hash] html_options Options passed to link_to
42 42 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
43 43 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
44 44 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
45 45 end
46 46
47 47 # Displays a link to user's account page if active
48 48 def link_to_user(user, options={})
49 49 if user.is_a?(User)
50 50 name = h(user.name(options[:format]))
51 51 if user.active? || (User.current.admin? && user.logged?)
52 52 link_to name, user_path(user), :class => user.css_classes
53 53 else
54 54 name
55 55 end
56 56 else
57 57 h(user.to_s)
58 58 end
59 59 end
60 60
61 61 # Displays a link to +issue+ with its subject.
62 62 # Examples:
63 63 #
64 64 # link_to_issue(issue) # => Defect #6: This is the subject
65 65 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
66 66 # link_to_issue(issue, :subject => false) # => Defect #6
67 67 # link_to_issue(issue, :project => true) # => Foo - Defect #6
68 68 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
69 69 #
70 70 def link_to_issue(issue, options={})
71 71 title = nil
72 72 subject = nil
73 73 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
74 74 if options[:subject] == false
75 75 title = issue.subject.truncate(60)
76 76 else
77 77 subject = issue.subject
78 78 if truncate_length = options[:truncate]
79 79 subject = subject.truncate(truncate_length)
80 80 end
81 81 end
82 82 only_path = options[:only_path].nil? ? true : options[:only_path]
83 83 s = link_to(text, issue_path(issue, :only_path => only_path),
84 84 :class => issue.css_classes, :title => title)
85 85 s << h(": #{subject}") if subject
86 86 s = h("#{issue.project} - ") + s if options[:project]
87 87 s
88 88 end
89 89
90 90 # Generates a link to an attachment.
91 91 # Options:
92 92 # * :text - Link text (default to attachment filename)
93 93 # * :download - Force download (default: false)
94 94 def link_to_attachment(attachment, options={})
95 95 text = options.delete(:text) || attachment.filename
96 96 route_method = options.delete(:download) ? :download_named_attachment_path : :named_attachment_path
97 97 html_options = options.slice!(:only_path)
98 98 url = send(route_method, attachment, attachment.filename, options)
99 99 link_to text, url, html_options
100 100 end
101 101
102 102 # Generates a link to a SCM revision
103 103 # Options:
104 104 # * :text - Link text (default to the formatted revision)
105 105 def link_to_revision(revision, repository, options={})
106 106 if repository.is_a?(Project)
107 107 repository = repository.repository
108 108 end
109 109 text = options.delete(:text) || format_revision(revision)
110 110 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
111 111 link_to(
112 112 h(text),
113 113 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
114 114 :title => l(:label_revision_id, format_revision(revision))
115 115 )
116 116 end
117 117
118 118 # Generates a link to a message
119 119 def link_to_message(message, options={}, html_options = nil)
120 120 link_to(
121 121 message.subject.truncate(60),
122 122 board_message_path(message.board_id, message.parent_id || message.id, {
123 123 :r => (message.parent_id && message.id),
124 124 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
125 125 }.merge(options)),
126 126 html_options
127 127 )
128 128 end
129 129
130 130 # Generates a link to a project if active
131 131 # Examples:
132 132 #
133 133 # link_to_project(project) # => link to the specified project overview
134 134 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
135 135 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
136 136 #
137 137 def link_to_project(project, options={}, html_options = nil)
138 138 if project.archived?
139 139 h(project.name)
140 140 elsif options.key?(:action)
141 141 ActiveSupport::Deprecation.warn "#link_to_project with :action option is deprecated and will be removed in Redmine 3.0."
142 142 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
143 143 link_to project.name, url, html_options
144 144 else
145 145 link_to project.name, project_path(project, options), html_options
146 146 end
147 147 end
148 148
149 149 # Generates a link to a project settings if active
150 150 def link_to_project_settings(project, options={}, html_options=nil)
151 151 if project.active?
152 152 link_to project.name, settings_project_path(project, options), html_options
153 153 elsif project.archived?
154 154 h(project.name)
155 155 else
156 156 link_to project.name, project_path(project, options), html_options
157 157 end
158 158 end
159 159
160 160 # Generates a link to a version
161 161 def link_to_version(version, options = {})
162 162 return '' unless version && version.is_a?(Version)
163 163 options = {:title => format_date(version.effective_date)}.merge(options)
164 164 link_to_if version.visible?, format_version_name(version), version_path(version), options
165 165 end
166 166
167 167 # Helper that formats object for html or text rendering
168 168 def format_object(object, html=true, &block)
169 169 if block_given?
170 170 object = yield object
171 171 end
172 172 case object.class.name
173 173 when 'Array'
174 174 object.map {|o| format_object(o, html)}.join(', ').html_safe
175 175 when 'Time'
176 176 format_time(object)
177 177 when 'Date'
178 178 format_date(object)
179 179 when 'Fixnum'
180 180 object.to_s
181 181 when 'Float'
182 182 sprintf "%.2f", object
183 183 when 'User'
184 184 html ? link_to_user(object) : object.to_s
185 185 when 'Project'
186 186 html ? link_to_project(object) : object.to_s
187 187 when 'Version'
188 188 html ? link_to_version(object) : object.to_s
189 189 when 'TrueClass'
190 190 l(:general_text_Yes)
191 191 when 'FalseClass'
192 192 l(:general_text_No)
193 193 when 'Issue'
194 194 object.visible? && html ? link_to_issue(object) : "##{object.id}"
195 195 when 'CustomValue', 'CustomFieldValue'
196 196 if object.custom_field
197 197 f = object.custom_field.format.formatted_custom_value(self, object, html)
198 198 if f.nil? || f.is_a?(String)
199 199 f
200 200 else
201 201 format_object(f, html, &block)
202 202 end
203 203 else
204 204 object.value.to_s
205 205 end
206 206 else
207 207 html ? h(object) : object.to_s
208 208 end
209 209 end
210 210
211 211 def wiki_page_path(page, options={})
212 212 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
213 213 end
214 214
215 215 def thumbnail_tag(attachment)
216 216 link_to image_tag(thumbnail_path(attachment)),
217 217 named_attachment_path(attachment, attachment.filename),
218 218 :title => attachment.filename
219 219 end
220 220
221 221 def toggle_link(name, id, options={})
222 222 onclick = "$('##{id}').toggle(); "
223 223 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
224 224 onclick << "return false;"
225 225 link_to(name, "#", :onclick => onclick)
226 226 end
227 227
228 228 def image_to_function(name, function, html_options = {})
229 229 html_options.symbolize_keys!
230 230 tag(:input, html_options.merge({
231 231 :type => "image", :src => image_path(name),
232 232 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
233 233 }))
234 234 end
235 235
236 236 def format_activity_title(text)
237 237 h(truncate_single_line_raw(text, 100))
238 238 end
239 239
240 240 def format_activity_day(date)
241 241 date == User.current.today ? l(:label_today).titleize : format_date(date)
242 242 end
243 243
244 244 def format_activity_description(text)
245 245 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
246 246 ).gsub(/[\r\n]+/, "<br />").html_safe
247 247 end
248 248
249 249 def format_version_name(version)
250 if version.project == @project
250 if !version.shared? || version.project == @project
251 251 h(version)
252 252 else
253 253 h("#{version.project} - #{version}")
254 254 end
255 255 end
256 256
257 257 def due_date_distance_in_words(date)
258 258 if date
259 259 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
260 260 end
261 261 end
262 262
263 263 # Renders a tree of projects as a nested set of unordered lists
264 264 # The given collection may be a subset of the whole project tree
265 265 # (eg. some intermediate nodes are private and can not be seen)
266 266 def render_project_nested_lists(projects)
267 267 s = ''
268 268 if projects.any?
269 269 ancestors = []
270 270 original_project = @project
271 271 projects.sort_by(&:lft).each do |project|
272 272 # set the project environment to please macros.
273 273 @project = project
274 274 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
275 275 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
276 276 else
277 277 ancestors.pop
278 278 s << "</li>"
279 279 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
280 280 ancestors.pop
281 281 s << "</ul></li>\n"
282 282 end
283 283 end
284 284 classes = (ancestors.empty? ? 'root' : 'child')
285 285 s << "<li class='#{classes}'><div class='#{classes}'>"
286 286 s << h(block_given? ? yield(project) : project.name)
287 287 s << "</div>\n"
288 288 ancestors << project
289 289 end
290 290 s << ("</li></ul>\n" * ancestors.size)
291 291 @project = original_project
292 292 end
293 293 s.html_safe
294 294 end
295 295
296 296 def render_page_hierarchy(pages, node=nil, options={})
297 297 content = ''
298 298 if pages[node]
299 299 content << "<ul class=\"pages-hierarchy\">\n"
300 300 pages[node].each do |page|
301 301 content << "<li>"
302 302 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
303 303 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
304 304 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
305 305 content << "</li>\n"
306 306 end
307 307 content << "</ul>\n"
308 308 end
309 309 content.html_safe
310 310 end
311 311
312 312 # Renders flash messages
313 313 def render_flash_messages
314 314 s = ''
315 315 flash.each do |k,v|
316 316 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
317 317 end
318 318 s.html_safe
319 319 end
320 320
321 321 # Renders tabs and their content
322 322 def render_tabs(tabs, selected=params[:tab])
323 323 if tabs.any?
324 324 unless tabs.detect {|tab| tab[:name] == selected}
325 325 selected = nil
326 326 end
327 327 selected ||= tabs.first[:name]
328 328 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
329 329 else
330 330 content_tag 'p', l(:label_no_data), :class => "nodata"
331 331 end
332 332 end
333 333
334 334 # Renders the project quick-jump box
335 335 def render_project_jump_box
336 336 return unless User.current.logged?
337 337 projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
338 338 if projects.any?
339 339 options =
340 340 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
341 341 '<option value="" disabled="disabled">---</option>').html_safe
342 342
343 343 options << project_tree_options_for_select(projects, :selected => @project) do |p|
344 344 { :value => project_path(:id => p, :jump => current_menu_item) }
345 345 end
346 346
347 347 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
348 348 end
349 349 end
350 350
351 351 def project_tree_options_for_select(projects, options = {})
352 352 s = ''
353 353 project_tree(projects) do |project, level|
354 354 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
355 355 tag_options = {:value => project.id}
356 356 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
357 357 tag_options[:selected] = 'selected'
358 358 else
359 359 tag_options[:selected] = nil
360 360 end
361 361 tag_options.merge!(yield(project)) if block_given?
362 362 s << content_tag('option', name_prefix + h(project), tag_options)
363 363 end
364 364 s.html_safe
365 365 end
366 366
367 367 # Yields the given block for each project with its level in the tree
368 368 #
369 369 # Wrapper for Project#project_tree
370 370 def project_tree(projects, &block)
371 371 Project.project_tree(projects, &block)
372 372 end
373 373
374 374 def principals_check_box_tags(name, principals)
375 375 s = ''
376 376 principals.each do |principal|
377 377 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
378 378 end
379 379 s.html_safe
380 380 end
381 381
382 382 # Returns a string for users/groups option tags
383 383 def principals_options_for_select(collection, selected=nil)
384 384 s = ''
385 385 if collection.include?(User.current)
386 386 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
387 387 end
388 388 groups = ''
389 389 collection.sort.each do |element|
390 390 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
391 391 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
392 392 end
393 393 unless groups.empty?
394 394 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
395 395 end
396 396 s.html_safe
397 397 end
398 398
399 399 # Options for the new membership projects combo-box
400 400 def options_for_membership_project_select(principal, projects)
401 401 options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
402 402 options << project_tree_options_for_select(projects) do |p|
403 403 {:disabled => principal.projects.to_a.include?(p)}
404 404 end
405 405 options
406 406 end
407 407
408 408 def option_tag(name, text, value, selected=nil, options={})
409 409 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
410 410 end
411 411
412 412 # Truncates and returns the string as a single line
413 413 def truncate_single_line(string, *args)
414 414 ActiveSupport::Deprecation.warn(
415 415 "ApplicationHelper#truncate_single_line is deprecated and will be removed in Rails 4 poring")
416 416 # Rails 4 ActionView::Helpers::TextHelper#truncate escapes.
417 417 # So, result is broken.
418 418 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
419 419 end
420 420
421 421 def truncate_single_line_raw(string, length)
422 422 string.truncate(length).gsub(%r{[\r\n]+}m, ' ')
423 423 end
424 424
425 425 # Truncates at line break after 250 characters or options[:length]
426 426 def truncate_lines(string, options={})
427 427 length = options[:length] || 250
428 428 if string.to_s =~ /\A(.{#{length}}.*?)$/m
429 429 "#{$1}..."
430 430 else
431 431 string
432 432 end
433 433 end
434 434
435 435 def anchor(text)
436 436 text.to_s.gsub(' ', '_')
437 437 end
438 438
439 439 def html_hours(text)
440 440 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
441 441 end
442 442
443 443 def authoring(created, author, options={})
444 444 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
445 445 end
446 446
447 447 def time_tag(time)
448 448 text = distance_of_time_in_words(Time.now, time)
449 449 if @project
450 450 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
451 451 else
452 452 content_tag('abbr', text, :title => format_time(time))
453 453 end
454 454 end
455 455
456 456 def syntax_highlight_lines(name, content)
457 457 lines = []
458 458 syntax_highlight(name, content).each_line { |line| lines << line }
459 459 lines
460 460 end
461 461
462 462 def syntax_highlight(name, content)
463 463 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
464 464 end
465 465
466 466 def to_path_param(path)
467 467 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
468 468 str.blank? ? nil : str
469 469 end
470 470
471 471 def reorder_links(name, url, method = :post)
472 472 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
473 473 url.merge({"#{name}[move_to]" => 'highest'}),
474 474 :method => method, :title => l(:label_sort_highest)) +
475 475 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
476 476 url.merge({"#{name}[move_to]" => 'higher'}),
477 477 :method => method, :title => l(:label_sort_higher)) +
478 478 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
479 479 url.merge({"#{name}[move_to]" => 'lower'}),
480 480 :method => method, :title => l(:label_sort_lower)) +
481 481 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
482 482 url.merge({"#{name}[move_to]" => 'lowest'}),
483 483 :method => method, :title => l(:label_sort_lowest))
484 484 end
485 485
486 486 def breadcrumb(*args)
487 487 elements = args.flatten
488 488 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
489 489 end
490 490
491 491 def other_formats_links(&block)
492 492 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
493 493 yield Redmine::Views::OtherFormatsBuilder.new(self)
494 494 concat('</p>'.html_safe)
495 495 end
496 496
497 497 def page_header_title
498 498 if @project.nil? || @project.new_record?
499 499 h(Setting.app_title)
500 500 else
501 501 b = []
502 502 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
503 503 if ancestors.any?
504 504 root = ancestors.shift
505 505 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
506 506 if ancestors.size > 2
507 507 b << "\xe2\x80\xa6"
508 508 ancestors = ancestors[-2, 2]
509 509 end
510 510 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
511 511 end
512 512 b << h(@project)
513 513 b.join(" \xc2\xbb ").html_safe
514 514 end
515 515 end
516 516
517 517 # Returns a h2 tag and sets the html title with the given arguments
518 518 def title(*args)
519 519 strings = args.map do |arg|
520 520 if arg.is_a?(Array) && arg.size >= 2
521 521 link_to(*arg)
522 522 else
523 523 h(arg.to_s)
524 524 end
525 525 end
526 526 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
527 527 content_tag('h2', strings.join(' &#187; ').html_safe)
528 528 end
529 529
530 530 # Sets the html title
531 531 # Returns the html title when called without arguments
532 532 # Current project name and app_title and automatically appended
533 533 # Exemples:
534 534 # html_title 'Foo', 'Bar'
535 535 # html_title # => 'Foo - Bar - My Project - Redmine'
536 536 def html_title(*args)
537 537 if args.empty?
538 538 title = @html_title || []
539 539 title << @project.name if @project
540 540 title << Setting.app_title unless Setting.app_title == title.last
541 541 title.reject(&:blank?).join(' - ')
542 542 else
543 543 @html_title ||= []
544 544 @html_title += args
545 545 end
546 546 end
547 547
548 548 # Returns the theme, controller name, and action as css classes for the
549 549 # HTML body.
550 550 def body_css_classes
551 551 css = []
552 552 if theme = Redmine::Themes.theme(Setting.ui_theme)
553 553 css << 'theme-' + theme.name
554 554 end
555 555
556 556 css << 'project-' + @project.identifier if @project && @project.identifier.present?
557 557 css << 'controller-' + controller_name
558 558 css << 'action-' + action_name
559 559 css.join(' ')
560 560 end
561 561
562 562 def accesskey(s)
563 563 @used_accesskeys ||= []
564 564 key = Redmine::AccessKeys.key_for(s)
565 565 return nil if @used_accesskeys.include?(key)
566 566 @used_accesskeys << key
567 567 key
568 568 end
569 569
570 570 # Formats text according to system settings.
571 571 # 2 ways to call this method:
572 572 # * with a String: textilizable(text, options)
573 573 # * with an object and one of its attribute: textilizable(issue, :description, options)
574 574 def textilizable(*args)
575 575 options = args.last.is_a?(Hash) ? args.pop : {}
576 576 case args.size
577 577 when 1
578 578 obj = options[:object]
579 579 text = args.shift
580 580 when 2
581 581 obj = args.shift
582 582 attr = args.shift
583 583 text = obj.send(attr).to_s
584 584 else
585 585 raise ArgumentError, 'invalid arguments to textilizable'
586 586 end
587 587 return '' if text.blank?
588 588 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
589 589 only_path = options.delete(:only_path) == false ? false : true
590 590
591 591 text = text.dup
592 592 macros = catch_macros(text)
593 593 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
594 594
595 595 @parsed_headings = []
596 596 @heading_anchors = {}
597 597 @current_section = 0 if options[:edit_section_links]
598 598
599 599 parse_sections(text, project, obj, attr, only_path, options)
600 600 text = parse_non_pre_blocks(text, obj, macros) do |text|
601 601 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
602 602 send method_name, text, project, obj, attr, only_path, options
603 603 end
604 604 end
605 605 parse_headings(text, project, obj, attr, only_path, options)
606 606
607 607 if @parsed_headings.any?
608 608 replace_toc(text, @parsed_headings)
609 609 end
610 610
611 611 text.html_safe
612 612 end
613 613
614 614 def parse_non_pre_blocks(text, obj, macros)
615 615 s = StringScanner.new(text)
616 616 tags = []
617 617 parsed = ''
618 618 while !s.eos?
619 619 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
620 620 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
621 621 if tags.empty?
622 622 yield text
623 623 inject_macros(text, obj, macros) if macros.any?
624 624 else
625 625 inject_macros(text, obj, macros, false) if macros.any?
626 626 end
627 627 parsed << text
628 628 if tag
629 629 if closing
630 630 if tags.last == tag.downcase
631 631 tags.pop
632 632 end
633 633 else
634 634 tags << tag.downcase
635 635 end
636 636 parsed << full_tag
637 637 end
638 638 end
639 639 # Close any non closing tags
640 640 while tag = tags.pop
641 641 parsed << "</#{tag}>"
642 642 end
643 643 parsed
644 644 end
645 645
646 646 def parse_inline_attachments(text, project, obj, attr, only_path, options)
647 647 # when using an image link, try to use an attachment, if possible
648 648 attachments = options[:attachments] || []
649 649 attachments += obj.attachments if obj.respond_to?(:attachments)
650 650 if attachments.present?
651 651 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
652 652 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
653 653 # search for the picture in attachments
654 654 if found = Attachment.latest_attach(attachments, filename)
655 655 image_url = download_named_attachment_path(found, found.filename, :only_path => only_path)
656 656 desc = found.description.to_s.gsub('"', '')
657 657 if !desc.blank? && alttext.blank?
658 658 alt = " title=\"#{desc}\" alt=\"#{desc}\""
659 659 end
660 660 "src=\"#{image_url}\"#{alt}"
661 661 else
662 662 m
663 663 end
664 664 end
665 665 end
666 666 end
667 667
668 668 # Wiki links
669 669 #
670 670 # Examples:
671 671 # [[mypage]]
672 672 # [[mypage|mytext]]
673 673 # wiki links can refer other project wikis, using project name or identifier:
674 674 # [[project:]] -> wiki starting page
675 675 # [[project:|mytext]]
676 676 # [[project:mypage]]
677 677 # [[project:mypage|mytext]]
678 678 def parse_wiki_links(text, project, obj, attr, only_path, options)
679 679 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
680 680 link_project = project
681 681 esc, all, page, title = $1, $2, $3, $5
682 682 if esc.nil?
683 683 if page =~ /^([^\:]+)\:(.*)$/
684 684 identifier, page = $1, $2
685 685 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
686 686 title ||= identifier if page.blank?
687 687 end
688 688
689 689 if link_project && link_project.wiki
690 690 # extract anchor
691 691 anchor = nil
692 692 if page =~ /^(.+?)\#(.+)$/
693 693 page, anchor = $1, $2
694 694 end
695 695 anchor = sanitize_anchor_name(anchor) if anchor.present?
696 696 # check if page exists
697 697 wiki_page = link_project.wiki.find_page(page)
698 698 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
699 699 "##{anchor}"
700 700 else
701 701 case options[:wiki_links]
702 702 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
703 703 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
704 704 else
705 705 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
706 706 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
707 707 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
708 708 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
709 709 end
710 710 end
711 711 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
712 712 else
713 713 # project or wiki doesn't exist
714 714 all
715 715 end
716 716 else
717 717 all
718 718 end
719 719 end
720 720 end
721 721
722 722 # Redmine links
723 723 #
724 724 # Examples:
725 725 # Issues:
726 726 # #52 -> Link to issue #52
727 727 # Changesets:
728 728 # r52 -> Link to revision 52
729 729 # commit:a85130f -> Link to scmid starting with a85130f
730 730 # Documents:
731 731 # document#17 -> Link to document with id 17
732 732 # document:Greetings -> Link to the document with title "Greetings"
733 733 # document:"Some document" -> Link to the document with title "Some document"
734 734 # Versions:
735 735 # version#3 -> Link to version with id 3
736 736 # version:1.0.0 -> Link to version named "1.0.0"
737 737 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
738 738 # Attachments:
739 739 # attachment:file.zip -> Link to the attachment of the current object named file.zip
740 740 # Source files:
741 741 # source:some/file -> Link to the file located at /some/file in the project's repository
742 742 # source:some/file@52 -> Link to the file's revision 52
743 743 # source:some/file#L120 -> Link to line 120 of the file
744 744 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
745 745 # export:some/file -> Force the download of the file
746 746 # Forum messages:
747 747 # message#1218 -> Link to message with id 1218
748 748 # Projects:
749 749 # project:someproject -> Link to project named "someproject"
750 750 # project#3 -> Link to project with id 3
751 751 #
752 752 # Links can refer other objects from other projects, using project identifier:
753 753 # identifier:r52
754 754 # identifier:document:"Some document"
755 755 # identifier:version:1.0.0
756 756 # identifier:source:some/file
757 757 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
758 758 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-_]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
759 759 leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $2, $3, $4, $5, $10, $11, $8 || $12 || $18, $14 || $19, $15, $17
760 760 link = nil
761 761 project = default_project
762 762 if project_identifier
763 763 project = Project.visible.find_by_identifier(project_identifier)
764 764 end
765 765 if esc.nil?
766 766 if prefix.nil? && sep == 'r'
767 767 if project
768 768 repository = nil
769 769 if repo_identifier
770 770 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
771 771 else
772 772 repository = project.repository
773 773 end
774 774 # project.changesets.visible raises an SQL error because of a double join on repositories
775 775 if repository &&
776 776 (changeset = Changeset.visible.
777 777 find_by_repository_id_and_revision(repository.id, identifier))
778 778 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
779 779 {:only_path => only_path, :controller => 'repositories',
780 780 :action => 'revision', :id => project,
781 781 :repository_id => repository.identifier_param,
782 782 :rev => changeset.revision},
783 783 :class => 'changeset',
784 784 :title => truncate_single_line_raw(changeset.comments, 100))
785 785 end
786 786 end
787 787 elsif sep == '#'
788 788 oid = identifier.to_i
789 789 case prefix
790 790 when nil
791 791 if oid.to_s == identifier &&
792 792 issue = Issue.visible.includes(:status).find_by_id(oid)
793 793 anchor = comment_id ? "note-#{comment_id}" : nil
794 794 link = link_to(h("##{oid}#{comment_suffix}"),
795 795 {:only_path => only_path, :controller => 'issues',
796 796 :action => 'show', :id => oid, :anchor => anchor},
797 797 :class => issue.css_classes,
798 798 :title => "#{issue.subject.truncate(100)} (#{issue.status.name})")
799 799 end
800 800 when 'document'
801 801 if document = Document.visible.find_by_id(oid)
802 802 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
803 803 :class => 'document'
804 804 end
805 805 when 'version'
806 806 if version = Version.visible.find_by_id(oid)
807 807 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
808 808 :class => 'version'
809 809 end
810 810 when 'message'
811 811 if message = Message.visible.includes(:parent).find_by_id(oid)
812 812 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
813 813 end
814 814 when 'forum'
815 815 if board = Board.visible.find_by_id(oid)
816 816 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
817 817 :class => 'board'
818 818 end
819 819 when 'news'
820 820 if news = News.visible.find_by_id(oid)
821 821 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
822 822 :class => 'news'
823 823 end
824 824 when 'project'
825 825 if p = Project.visible.find_by_id(oid)
826 826 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
827 827 end
828 828 end
829 829 elsif sep == ':'
830 830 # removes the double quotes if any
831 831 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
832 832 name = CGI.unescapeHTML(name)
833 833 case prefix
834 834 when 'document'
835 835 if project && document = project.documents.visible.find_by_title(name)
836 836 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
837 837 :class => 'document'
838 838 end
839 839 when 'version'
840 840 if project && version = project.versions.visible.find_by_name(name)
841 841 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
842 842 :class => 'version'
843 843 end
844 844 when 'forum'
845 845 if project && board = project.boards.visible.find_by_name(name)
846 846 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
847 847 :class => 'board'
848 848 end
849 849 when 'news'
850 850 if project && news = project.news.visible.find_by_title(name)
851 851 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
852 852 :class => 'news'
853 853 end
854 854 when 'commit', 'source', 'export'
855 855 if project
856 856 repository = nil
857 857 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
858 858 repo_prefix, repo_identifier, name = $1, $2, $3
859 859 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
860 860 else
861 861 repository = project.repository
862 862 end
863 863 if prefix == 'commit'
864 864 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
865 865 link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
866 866 :class => 'changeset',
867 867 :title => truncate_single_line_raw(changeset.comments, 100)
868 868 end
869 869 else
870 870 if repository && User.current.allowed_to?(:browse_repository, project)
871 871 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
872 872 path, rev, anchor = $1, $3, $5
873 873 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
874 874 :path => to_path_param(path),
875 875 :rev => rev,
876 876 :anchor => anchor},
877 877 :class => (prefix == 'export' ? 'source download' : 'source')
878 878 end
879 879 end
880 880 repo_prefix = nil
881 881 end
882 882 when 'attachment'
883 883 attachments = options[:attachments] || []
884 884 attachments += obj.attachments if obj.respond_to?(:attachments)
885 885 if attachments && attachment = Attachment.latest_attach(attachments, name)
886 886 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
887 887 end
888 888 when 'project'
889 889 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
890 890 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
891 891 end
892 892 end
893 893 end
894 894 end
895 895 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
896 896 end
897 897 end
898 898
899 899 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
900 900
901 901 def parse_sections(text, project, obj, attr, only_path, options)
902 902 return unless options[:edit_section_links]
903 903 text.gsub!(HEADING_RE) do
904 904 heading = $1
905 905 @current_section += 1
906 906 if @current_section > 1
907 907 content_tag('div',
908 908 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
909 909 :class => 'contextual',
910 910 :title => l(:button_edit_section),
911 911 :id => "section-#{@current_section}") + heading.html_safe
912 912 else
913 913 heading
914 914 end
915 915 end
916 916 end
917 917
918 918 # Headings and TOC
919 919 # Adds ids and links to headings unless options[:headings] is set to false
920 920 def parse_headings(text, project, obj, attr, only_path, options)
921 921 return if options[:headings] == false
922 922
923 923 text.gsub!(HEADING_RE) do
924 924 level, attrs, content = $2.to_i, $3, $4
925 925 item = strip_tags(content).strip
926 926 anchor = sanitize_anchor_name(item)
927 927 # used for single-file wiki export
928 928 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
929 929 @heading_anchors[anchor] ||= 0
930 930 idx = (@heading_anchors[anchor] += 1)
931 931 if idx > 1
932 932 anchor = "#{anchor}-#{idx}"
933 933 end
934 934 @parsed_headings << [level, anchor, item]
935 935 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
936 936 end
937 937 end
938 938
939 939 MACROS_RE = /(
940 940 (!)? # escaping
941 941 (
942 942 \{\{ # opening tag
943 943 ([\w]+) # macro name
944 944 (\(([^\n\r]*?)\))? # optional arguments
945 945 ([\n\r].*?[\n\r])? # optional block of text
946 946 \}\} # closing tag
947 947 )
948 948 )/mx unless const_defined?(:MACROS_RE)
949 949
950 950 MACRO_SUB_RE = /(
951 951 \{\{
952 952 macro\((\d+)\)
953 953 \}\}
954 954 )/x unless const_defined?(:MACRO_SUB_RE)
955 955
956 956 # Extracts macros from text
957 957 def catch_macros(text)
958 958 macros = {}
959 959 text.gsub!(MACROS_RE) do
960 960 all, macro = $1, $4.downcase
961 961 if macro_exists?(macro) || all =~ MACRO_SUB_RE
962 962 index = macros.size
963 963 macros[index] = all
964 964 "{{macro(#{index})}}"
965 965 else
966 966 all
967 967 end
968 968 end
969 969 macros
970 970 end
971 971
972 972 # Executes and replaces macros in text
973 973 def inject_macros(text, obj, macros, execute=true)
974 974 text.gsub!(MACRO_SUB_RE) do
975 975 all, index = $1, $2.to_i
976 976 orig = macros.delete(index)
977 977 if execute && orig && orig =~ MACROS_RE
978 978 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
979 979 if esc.nil?
980 980 h(exec_macro(macro, obj, args, block) || all)
981 981 else
982 982 h(all)
983 983 end
984 984 elsif orig
985 985 h(orig)
986 986 else
987 987 h(all)
988 988 end
989 989 end
990 990 end
991 991
992 992 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
993 993
994 994 # Renders the TOC with given headings
995 995 def replace_toc(text, headings)
996 996 text.gsub!(TOC_RE) do
997 997 left_align, right_align = $2, $3
998 998 # Keep only the 4 first levels
999 999 headings = headings.select{|level, anchor, item| level <= 4}
1000 1000 if headings.empty?
1001 1001 ''
1002 1002 else
1003 1003 div_class = 'toc'
1004 1004 div_class << ' right' if right_align
1005 1005 div_class << ' left' if left_align
1006 1006 out = "<ul class=\"#{div_class}\"><li>"
1007 1007 root = headings.map(&:first).min
1008 1008 current = root
1009 1009 started = false
1010 1010 headings.each do |level, anchor, item|
1011 1011 if level > current
1012 1012 out << '<ul><li>' * (level - current)
1013 1013 elsif level < current
1014 1014 out << "</li></ul>\n" * (current - level) + "</li><li>"
1015 1015 elsif started
1016 1016 out << '</li><li>'
1017 1017 end
1018 1018 out << "<a href=\"##{anchor}\">#{item}</a>"
1019 1019 current = level
1020 1020 started = true
1021 1021 end
1022 1022 out << '</li></ul>' * (current - root)
1023 1023 out << '</li></ul>'
1024 1024 end
1025 1025 end
1026 1026 end
1027 1027
1028 1028 # Same as Rails' simple_format helper without using paragraphs
1029 1029 def simple_format_without_paragraph(text)
1030 1030 text.to_s.
1031 1031 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1032 1032 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1033 1033 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1034 1034 html_safe
1035 1035 end
1036 1036
1037 1037 def lang_options_for_select(blank=true)
1038 1038 (blank ? [["(auto)", ""]] : []) + languages_options
1039 1039 end
1040 1040
1041 1041 def label_tag_for(name, option_tags = nil, options = {})
1042 1042 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
1043 1043 content_tag("label", label_text)
1044 1044 end
1045 1045
1046 1046 def labelled_form_for(*args, &proc)
1047 1047 args << {} unless args.last.is_a?(Hash)
1048 1048 options = args.last
1049 1049 if args.first.is_a?(Symbol)
1050 1050 options.merge!(:as => args.shift)
1051 1051 end
1052 1052 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1053 1053 form_for(*args, &proc)
1054 1054 end
1055 1055
1056 1056 def labelled_fields_for(*args, &proc)
1057 1057 args << {} unless args.last.is_a?(Hash)
1058 1058 options = args.last
1059 1059 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1060 1060 fields_for(*args, &proc)
1061 1061 end
1062 1062
1063 1063 def labelled_remote_form_for(*args, &proc)
1064 1064 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_remote_form_for is deprecated and will be removed in Redmine 2.2."
1065 1065 args << {} unless args.last.is_a?(Hash)
1066 1066 options = args.last
1067 1067 options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
1068 1068 form_for(*args, &proc)
1069 1069 end
1070 1070
1071 1071 def error_messages_for(*objects)
1072 1072 html = ""
1073 1073 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1074 1074 errors = objects.map {|o| o.errors.full_messages}.flatten
1075 1075 if errors.any?
1076 1076 html << "<div id='errorExplanation'><ul>\n"
1077 1077 errors.each do |error|
1078 1078 html << "<li>#{h error}</li>\n"
1079 1079 end
1080 1080 html << "</ul></div>\n"
1081 1081 end
1082 1082 html.html_safe
1083 1083 end
1084 1084
1085 1085 def delete_link(url, options={})
1086 1086 options = {
1087 1087 :method => :delete,
1088 1088 :data => {:confirm => l(:text_are_you_sure)},
1089 1089 :class => 'icon icon-del'
1090 1090 }.merge(options)
1091 1091
1092 1092 link_to l(:button_delete), url, options
1093 1093 end
1094 1094
1095 1095 def preview_link(url, form, target='preview', options={})
1096 1096 content_tag 'a', l(:label_preview), {
1097 1097 :href => "#",
1098 1098 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1099 1099 :accesskey => accesskey(:preview)
1100 1100 }.merge(options)
1101 1101 end
1102 1102
1103 1103 def link_to_function(name, function, html_options={})
1104 1104 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1105 1105 end
1106 1106
1107 1107 # Helper to render JSON in views
1108 1108 def raw_json(arg)
1109 1109 arg.to_json.to_s.gsub('/', '\/').html_safe
1110 1110 end
1111 1111
1112 1112 def back_url
1113 1113 url = params[:back_url]
1114 1114 if url.nil? && referer = request.env['HTTP_REFERER']
1115 1115 url = CGI.unescape(referer.to_s)
1116 1116 end
1117 1117 url
1118 1118 end
1119 1119
1120 1120 def back_url_hidden_field_tag
1121 1121 url = back_url
1122 1122 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1123 1123 end
1124 1124
1125 1125 def check_all_links(form_name)
1126 1126 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1127 1127 " | ".html_safe +
1128 1128 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1129 1129 end
1130 1130
1131 1131 def progress_bar(pcts, options={})
1132 1132 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1133 1133 pcts = pcts.collect(&:round)
1134 1134 pcts[1] = pcts[1] - pcts[0]
1135 1135 pcts << (100 - pcts[1] - pcts[0])
1136 1136 width = options[:width] || '100px;'
1137 1137 legend = options[:legend] || ''
1138 1138 content_tag('table',
1139 1139 content_tag('tr',
1140 1140 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1141 1141 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1142 1142 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1143 1143 ), :class => "progress progress-#{pcts[0]}", :style => "width: #{width};").html_safe +
1144 1144 content_tag('p', legend, :class => 'percent').html_safe
1145 1145 end
1146 1146
1147 1147 def checked_image(checked=true)
1148 1148 if checked
1149 1149 image_tag 'toggle_check.png'
1150 1150 end
1151 1151 end
1152 1152
1153 1153 def context_menu(url)
1154 1154 unless @context_menu_included
1155 1155 content_for :header_tags do
1156 1156 javascript_include_tag('context_menu') +
1157 1157 stylesheet_link_tag('context_menu')
1158 1158 end
1159 1159 if l(:direction) == 'rtl'
1160 1160 content_for :header_tags do
1161 1161 stylesheet_link_tag('context_menu_rtl')
1162 1162 end
1163 1163 end
1164 1164 @context_menu_included = true
1165 1165 end
1166 1166 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1167 1167 end
1168 1168
1169 1169 def calendar_for(field_id)
1170 1170 include_calendar_headers_tags
1171 1171 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1172 1172 end
1173 1173
1174 1174 def include_calendar_headers_tags
1175 1175 unless @calendar_headers_tags_included
1176 1176 tags = javascript_include_tag("datepicker")
1177 1177 @calendar_headers_tags_included = true
1178 1178 content_for :header_tags do
1179 1179 start_of_week = Setting.start_of_week
1180 1180 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1181 1181 # Redmine uses 1..7 (monday..sunday) in settings and locales
1182 1182 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1183 1183 start_of_week = start_of_week.to_i % 7
1184 1184 tags << javascript_tag(
1185 1185 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1186 1186 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1187 1187 path_to_image('/images/calendar.png') +
1188 1188 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1189 1189 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1190 1190 "beforeShow: beforeShowDatePicker};")
1191 1191 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1192 1192 unless jquery_locale == 'en'
1193 1193 tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js")
1194 1194 end
1195 1195 tags
1196 1196 end
1197 1197 end
1198 1198 end
1199 1199
1200 1200 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1201 1201 # Examples:
1202 1202 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1203 1203 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1204 1204 #
1205 1205 def stylesheet_link_tag(*sources)
1206 1206 options = sources.last.is_a?(Hash) ? sources.pop : {}
1207 1207 plugin = options.delete(:plugin)
1208 1208 sources = sources.map do |source|
1209 1209 if plugin
1210 1210 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1211 1211 elsif current_theme && current_theme.stylesheets.include?(source)
1212 1212 current_theme.stylesheet_path(source)
1213 1213 else
1214 1214 source
1215 1215 end
1216 1216 end
1217 1217 super sources, options
1218 1218 end
1219 1219
1220 1220 # Overrides Rails' image_tag with themes and plugins support.
1221 1221 # Examples:
1222 1222 # image_tag('image.png') # => picks image.png from the current theme or defaults
1223 1223 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1224 1224 #
1225 1225 def image_tag(source, options={})
1226 1226 if plugin = options.delete(:plugin)
1227 1227 source = "/plugin_assets/#{plugin}/images/#{source}"
1228 1228 elsif current_theme && current_theme.images.include?(source)
1229 1229 source = current_theme.image_path(source)
1230 1230 end
1231 1231 super source, options
1232 1232 end
1233 1233
1234 1234 # Overrides Rails' javascript_include_tag with plugins support
1235 1235 # Examples:
1236 1236 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1237 1237 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1238 1238 #
1239 1239 def javascript_include_tag(*sources)
1240 1240 options = sources.last.is_a?(Hash) ? sources.pop : {}
1241 1241 if plugin = options.delete(:plugin)
1242 1242 sources = sources.map do |source|
1243 1243 if plugin
1244 1244 "/plugin_assets/#{plugin}/javascripts/#{source}"
1245 1245 else
1246 1246 source
1247 1247 end
1248 1248 end
1249 1249 end
1250 1250 super sources, options
1251 1251 end
1252 1252
1253 1253 # TODO: remove this in 2.5.0
1254 1254 def has_content?(name)
1255 1255 content_for?(name)
1256 1256 end
1257 1257
1258 1258 def sidebar_content?
1259 1259 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1260 1260 end
1261 1261
1262 1262 def view_layouts_base_sidebar_hook_response
1263 1263 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1264 1264 end
1265 1265
1266 1266 def email_delivery_enabled?
1267 1267 !!ActionMailer::Base.perform_deliveries
1268 1268 end
1269 1269
1270 1270 # Returns the avatar image tag for the given +user+ if avatars are enabled
1271 1271 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1272 1272 def avatar(user, options = { })
1273 1273 if Setting.gravatar_enabled?
1274 1274 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1275 1275 email = nil
1276 1276 if user.respond_to?(:mail)
1277 1277 email = user.mail
1278 1278 elsif user.to_s =~ %r{<(.+?)>}
1279 1279 email = $1
1280 1280 end
1281 1281 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1282 1282 else
1283 1283 ''
1284 1284 end
1285 1285 end
1286 1286
1287 1287 def sanitize_anchor_name(anchor)
1288 1288 if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java'
1289 1289 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1290 1290 else
1291 1291 # TODO: remove when ruby1.8 is no longer supported
1292 1292 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1293 1293 end
1294 1294 end
1295 1295
1296 1296 # Returns the javascript tags that are included in the html layout head
1297 1297 def javascript_heads
1298 1298 tags = javascript_include_tag('jquery-1.8.3-ui-1.9.2-ujs-2.0.3', 'application')
1299 1299 unless User.current.pref.warn_on_leaving_unsaved == '0'
1300 1300 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1301 1301 end
1302 1302 tags
1303 1303 end
1304 1304
1305 1305 def favicon
1306 1306 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1307 1307 end
1308 1308
1309 1309 # Returns the path to the favicon
1310 1310 def favicon_path
1311 1311 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1312 1312 image_path(icon)
1313 1313 end
1314 1314
1315 1315 # Returns the full URL to the favicon
1316 1316 def favicon_url
1317 1317 # TODO: use #image_url introduced in Rails4
1318 1318 path = favicon_path
1319 1319 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1320 1320 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1321 1321 end
1322 1322
1323 1323 def robot_exclusion_tag
1324 1324 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1325 1325 end
1326 1326
1327 1327 # Returns true if arg is expected in the API response
1328 1328 def include_in_api_response?(arg)
1329 1329 unless @included_in_api_response
1330 1330 param = params[:include]
1331 1331 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1332 1332 @included_in_api_response.collect!(&:strip)
1333 1333 end
1334 1334 @included_in_api_response.include?(arg.to_s)
1335 1335 end
1336 1336
1337 1337 # Returns options or nil if nometa param or X-Redmine-Nometa header
1338 1338 # was set in the request
1339 1339 def api_meta(options)
1340 1340 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1341 1341 # compatibility mode for activeresource clients that raise
1342 1342 # an error when deserializing an array with attributes
1343 1343 nil
1344 1344 else
1345 1345 options
1346 1346 end
1347 1347 end
1348 1348
1349 1349 private
1350 1350
1351 1351 def wiki_helper
1352 1352 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1353 1353 extend helper
1354 1354 return self
1355 1355 end
1356 1356
1357 1357 def link_to_content_update(text, url_params = {}, html_options = {})
1358 1358 link_to(text, url_params, html_options)
1359 1359 end
1360 1360 end
@@ -1,290 +1,295
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2014 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 Version < ActiveRecord::Base
19 19 include Redmine::SafeAttributes
20 20 after_update :update_issues_from_sharing_change
21 21 belongs_to :project
22 22 has_many :fixed_issues, :class_name => 'Issue', :foreign_key => 'fixed_version_id', :dependent => :nullify
23 23 acts_as_customizable
24 24 acts_as_attachable :view_permission => :view_files,
25 25 :delete_permission => :manage_files
26 26
27 27 VERSION_STATUSES = %w(open locked closed)
28 28 VERSION_SHARINGS = %w(none descendants hierarchy tree system)
29 29
30 30 validates_presence_of :name
31 31 validates_uniqueness_of :name, :scope => [:project_id]
32 32 validates_length_of :name, :maximum => 60
33 33 validates :effective_date, :date => true
34 34 validates_inclusion_of :status, :in => VERSION_STATUSES
35 35 validates_inclusion_of :sharing, :in => VERSION_SHARINGS
36 36
37 37 scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)}
38 38 scope :open, lambda { where(:status => 'open') }
39 39 scope :visible, lambda {|*args|
40 40 includes(:project).where(Project.allowed_to_condition(args.first || User.current, :view_issues))
41 41 }
42 42
43 43 safe_attributes 'name',
44 44 'description',
45 45 'effective_date',
46 46 'due_date',
47 47 'wiki_page_title',
48 48 'status',
49 49 'sharing',
50 50 'custom_field_values',
51 51 'custom_fields'
52 52
53 53 # Returns true if +user+ or current user is allowed to view the version
54 54 def visible?(user=User.current)
55 55 user.allowed_to?(:view_issues, self.project)
56 56 end
57 57
58 58 # Version files have same visibility as project files
59 59 def attachments_visible?(*args)
60 60 project.present? && project.attachments_visible?(*args)
61 61 end
62 62
63 63 def start_date
64 64 @start_date ||= fixed_issues.minimum('start_date')
65 65 end
66 66
67 67 def due_date
68 68 effective_date
69 69 end
70 70
71 71 def due_date=(arg)
72 72 self.effective_date=(arg)
73 73 end
74 74
75 75 # Returns the total estimated time for this version
76 76 # (sum of leaves estimated_hours)
77 77 def estimated_hours
78 78 @estimated_hours ||= fixed_issues.leaves.sum(:estimated_hours).to_f
79 79 end
80 80
81 81 # Returns the total reported time for this version
82 82 def spent_hours
83 83 @spent_hours ||= TimeEntry.joins(:issue).where("#{Issue.table_name}.fixed_version_id = ?", id).sum(:hours).to_f
84 84 end
85 85
86 86 def closed?
87 87 status == 'closed'
88 88 end
89 89
90 90 def open?
91 91 status == 'open'
92 92 end
93 93
94 94 # Returns true if the version is completed: due date reached and no open issues
95 95 def completed?
96 96 effective_date && (effective_date < Date.today) && (open_issues_count == 0)
97 97 end
98 98
99 99 def behind_schedule?
100 100 if completed_percent == 100
101 101 return false
102 102 elsif due_date && start_date
103 103 done_date = start_date + ((due_date - start_date+1)* completed_percent/100).floor
104 104 return done_date <= Date.today
105 105 else
106 106 false # No issues so it's not late
107 107 end
108 108 end
109 109
110 110 # Returns the completion percentage of this version based on the amount of open/closed issues
111 111 # and the time spent on the open issues.
112 112 def completed_percent
113 113 if issues_count == 0
114 114 0
115 115 elsif open_issues_count == 0
116 116 100
117 117 else
118 118 issues_progress(false) + issues_progress(true)
119 119 end
120 120 end
121 121
122 122 # TODO: remove in Redmine 3.0
123 123 def completed_pourcent
124 124 ActiveSupport::Deprecation.warn "Version#completed_pourcent is deprecated and will be removed in Redmine 3.0. Please use #completed_percent instead."
125 125 completed_percent
126 126 end
127 127
128 128 # Returns the percentage of issues that have been marked as 'closed'.
129 129 def closed_percent
130 130 if issues_count == 0
131 131 0
132 132 else
133 133 issues_progress(false)
134 134 end
135 135 end
136 136
137 137 # TODO: remove in Redmine 3.0
138 138 def closed_pourcent
139 139 ActiveSupport::Deprecation.warn "Version#closed_pourcent is deprecated and will be removed in Redmine 3.0. Please use #closed_percent instead."
140 140 closed_percent
141 141 end
142 142
143 143 # Returns true if the version is overdue: due date reached and some open issues
144 144 def overdue?
145 145 effective_date && (effective_date < Date.today) && (open_issues_count > 0)
146 146 end
147 147
148 148 # Returns assigned issues count
149 149 def issues_count
150 150 load_issue_counts
151 151 @issue_count
152 152 end
153 153
154 154 # Returns the total amount of open issues for this version.
155 155 def open_issues_count
156 156 load_issue_counts
157 157 @open_issues_count
158 158 end
159 159
160 160 # Returns the total amount of closed issues for this version.
161 161 def closed_issues_count
162 162 load_issue_counts
163 163 @closed_issues_count
164 164 end
165 165
166 166 def wiki_page
167 167 if project.wiki && !wiki_page_title.blank?
168 168 @wiki_page ||= project.wiki.find_page(wiki_page_title)
169 169 end
170 170 @wiki_page
171 171 end
172 172
173 173 def to_s; name end
174 174
175 175 def to_s_with_project
176 176 "#{project} - #{name}"
177 177 end
178 178
179 179 # Versions are sorted by effective_date and name
180 180 # Those with no effective_date are at the end, sorted by name
181 181 def <=>(version)
182 182 if self.effective_date
183 183 if version.effective_date
184 184 if self.effective_date == version.effective_date
185 185 name == version.name ? id <=> version.id : name <=> version.name
186 186 else
187 187 self.effective_date <=> version.effective_date
188 188 end
189 189 else
190 190 -1
191 191 end
192 192 else
193 193 if version.effective_date
194 194 1
195 195 else
196 196 name == version.name ? id <=> version.id : name <=> version.name
197 197 end
198 198 end
199 199 end
200 200
201 201 def self.fields_for_order_statement(table=nil)
202 202 table ||= table_name
203 203 ["(CASE WHEN #{table}.effective_date IS NULL THEN 1 ELSE 0 END)", "#{table}.effective_date", "#{table}.name", "#{table}.id"]
204 204 end
205 205
206 206 scope :sorted, lambda { order(fields_for_order_statement) }
207 207
208 208 # Returns the sharings that +user+ can set the version to
209 209 def allowed_sharings(user = User.current)
210 210 VERSION_SHARINGS.select do |s|
211 211 if sharing == s
212 212 true
213 213 else
214 214 case s
215 215 when 'system'
216 216 # Only admin users can set a systemwide sharing
217 217 user.admin?
218 218 when 'hierarchy', 'tree'
219 219 # Only users allowed to manage versions of the root project can
220 220 # set sharing to hierarchy or tree
221 221 project.nil? || user.allowed_to?(:manage_versions, project.root)
222 222 else
223 223 true
224 224 end
225 225 end
226 226 end
227 227 end
228 228
229 # Returns true if the version is shared, otherwise false
230 def shared?
231 sharing != 'none'
232 end
233
229 234 private
230 235
231 236 def load_issue_counts
232 237 unless @issue_count
233 238 @open_issues_count = 0
234 239 @closed_issues_count = 0
235 240 fixed_issues.group(:status).count.each do |status, count|
236 241 if status.is_closed?
237 242 @closed_issues_count += count
238 243 else
239 244 @open_issues_count += count
240 245 end
241 246 end
242 247 @issue_count = @open_issues_count + @closed_issues_count
243 248 end
244 249 end
245 250
246 251 # Update the issue's fixed versions. Used if a version's sharing changes.
247 252 def update_issues_from_sharing_change
248 253 if sharing_changed?
249 254 if VERSION_SHARINGS.index(sharing_was).nil? ||
250 255 VERSION_SHARINGS.index(sharing).nil? ||
251 256 VERSION_SHARINGS.index(sharing_was) > VERSION_SHARINGS.index(sharing)
252 257 Issue.update_versions_from_sharing_change self
253 258 end
254 259 end
255 260 end
256 261
257 262 # Returns the average estimated time of assigned issues
258 263 # or 1 if no issue has an estimated time
259 264 # Used to weight unestimated issues in progress calculation
260 265 def estimated_average
261 266 if @estimated_average.nil?
262 267 average = fixed_issues.average(:estimated_hours).to_f
263 268 if average == 0
264 269 average = 1
265 270 end
266 271 @estimated_average = average
267 272 end
268 273 @estimated_average
269 274 end
270 275
271 276 # Returns the total progress of open or closed issues. The returned percentage takes into account
272 277 # the amount of estimated time set for this version.
273 278 #
274 279 # Examples:
275 280 # issues_progress(true) => returns the progress percentage for open issues.
276 281 # issues_progress(false) => returns the progress percentage for closed issues.
277 282 def issues_progress(open)
278 283 @issues_progress ||= {}
279 284 @issues_progress[open] ||= begin
280 285 progress = 0
281 286 if issues_count > 0
282 287 ratio = open ? 'done_ratio' : 100
283 288
284 289 done = fixed_issues.open(open).sum("COALESCE(estimated_hours, #{estimated_average}) * #{ratio}").to_f
285 290 progress = done / (estimated_average * issues_count)
286 291 end
287 292 progress
288 293 end
289 294 end
290 295 end
@@ -1,3998 +1,3998
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2014 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
20 20 class IssuesControllerTest < ActionController::TestCase
21 21 fixtures :projects,
22 22 :users,
23 23 :roles,
24 24 :members,
25 25 :member_roles,
26 26 :issues,
27 27 :issue_statuses,
28 28 :versions,
29 29 :trackers,
30 30 :projects_trackers,
31 31 :issue_categories,
32 32 :enabled_modules,
33 33 :enumerations,
34 34 :attachments,
35 35 :workflows,
36 36 :custom_fields,
37 37 :custom_values,
38 38 :custom_fields_projects,
39 39 :custom_fields_trackers,
40 40 :time_entries,
41 41 :journals,
42 42 :journal_details,
43 43 :queries,
44 44 :repositories,
45 45 :changesets
46 46
47 47 include Redmine::I18n
48 48
49 49 def setup
50 50 User.current = nil
51 51 end
52 52
53 53 def test_index
54 54 with_settings :default_language => "en" do
55 55 get :index
56 56 assert_response :success
57 57 assert_template 'index'
58 58 assert_not_nil assigns(:issues)
59 59 assert_nil assigns(:project)
60 60
61 61 # links to visible issues
62 62 assert_select 'a[href=/issues/1]', :text => /#{ESCAPED_UCANT} print recipes/
63 63 assert_select 'a[href=/issues/5]', :text => /Subproject issue/
64 64 # private projects hidden
65 65 assert_select 'a[href=/issues/6]', 0
66 66 assert_select 'a[href=/issues/4]', 0
67 67 # project column
68 68 assert_select 'th', :text => /Project/
69 69 end
70 70 end
71 71
72 72 def test_index_should_not_list_issues_when_module_disabled
73 73 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
74 74 get :index
75 75 assert_response :success
76 76 assert_template 'index'
77 77 assert_not_nil assigns(:issues)
78 78 assert_nil assigns(:project)
79 79
80 80 assert_select 'a[href=/issues/1]', 0
81 81 assert_select 'a[href=/issues/5]', :text => /Subproject issue/
82 82 end
83 83
84 84 def test_index_should_list_visible_issues_only
85 85 get :index, :per_page => 100
86 86 assert_response :success
87 87 assert_not_nil assigns(:issues)
88 88 assert_nil assigns(:issues).detect {|issue| !issue.visible?}
89 89 end
90 90
91 91 def test_index_with_project
92 92 Setting.display_subprojects_issues = 0
93 93 get :index, :project_id => 1
94 94 assert_response :success
95 95 assert_template 'index'
96 96 assert_not_nil assigns(:issues)
97 97
98 98 assert_select 'a[href=/issues/1]', :text => /#{ESCAPED_UCANT} print recipes/
99 99 assert_select 'a[href=/issues/5]', 0
100 100 end
101 101
102 102 def test_index_with_project_and_subprojects
103 103 Setting.display_subprojects_issues = 1
104 104 get :index, :project_id => 1
105 105 assert_response :success
106 106 assert_template 'index'
107 107 assert_not_nil assigns(:issues)
108 108
109 109 assert_select 'a[href=/issues/1]', :text => /#{ESCAPED_UCANT} print recipes/
110 110 assert_select 'a[href=/issues/5]', :text => /Subproject issue/
111 111 assert_select 'a[href=/issues/6]', 0
112 112 end
113 113
114 114 def test_index_with_project_and_subprojects_should_show_private_subprojects_with_permission
115 115 @request.session[:user_id] = 2
116 116 Setting.display_subprojects_issues = 1
117 117 get :index, :project_id => 1
118 118 assert_response :success
119 119 assert_template 'index'
120 120 assert_not_nil assigns(:issues)
121 121
122 122 assert_select 'a[href=/issues/1]', :text => /#{ESCAPED_UCANT} print recipes/
123 123 assert_select 'a[href=/issues/5]', :text => /Subproject issue/
124 124 assert_select 'a[href=/issues/6]', :text => /Issue of a private subproject/
125 125 end
126 126
127 127 def test_index_with_project_and_default_filter
128 128 get :index, :project_id => 1, :set_filter => 1
129 129 assert_response :success
130 130 assert_template 'index'
131 131 assert_not_nil assigns(:issues)
132 132
133 133 query = assigns(:query)
134 134 assert_not_nil query
135 135 # default filter
136 136 assert_equal({'status_id' => {:operator => 'o', :values => ['']}}, query.filters)
137 137 end
138 138
139 139 def test_index_with_project_and_filter
140 140 get :index, :project_id => 1, :set_filter => 1,
141 141 :f => ['tracker_id'],
142 142 :op => {'tracker_id' => '='},
143 143 :v => {'tracker_id' => ['1']}
144 144 assert_response :success
145 145 assert_template 'index'
146 146 assert_not_nil assigns(:issues)
147 147
148 148 query = assigns(:query)
149 149 assert_not_nil query
150 150 assert_equal({'tracker_id' => {:operator => '=', :values => ['1']}}, query.filters)
151 151 end
152 152
153 153 def test_index_with_short_filters
154 154 to_test = {
155 155 'status_id' => {
156 156 'o' => { :op => 'o', :values => [''] },
157 157 'c' => { :op => 'c', :values => [''] },
158 158 '7' => { :op => '=', :values => ['7'] },
159 159 '7|3|4' => { :op => '=', :values => ['7', '3', '4'] },
160 160 '=7' => { :op => '=', :values => ['7'] },
161 161 '!3' => { :op => '!', :values => ['3'] },
162 162 '!7|3|4' => { :op => '!', :values => ['7', '3', '4'] }},
163 163 'subject' => {
164 164 'This is a subject' => { :op => '=', :values => ['This is a subject'] },
165 165 'o' => { :op => '=', :values => ['o'] },
166 166 '~This is part of a subject' => { :op => '~', :values => ['This is part of a subject'] },
167 167 '!~This is part of a subject' => { :op => '!~', :values => ['This is part of a subject'] }},
168 168 'tracker_id' => {
169 169 '3' => { :op => '=', :values => ['3'] },
170 170 '=3' => { :op => '=', :values => ['3'] }},
171 171 'start_date' => {
172 172 '2011-10-12' => { :op => '=', :values => ['2011-10-12'] },
173 173 '=2011-10-12' => { :op => '=', :values => ['2011-10-12'] },
174 174 '>=2011-10-12' => { :op => '>=', :values => ['2011-10-12'] },
175 175 '<=2011-10-12' => { :op => '<=', :values => ['2011-10-12'] },
176 176 '><2011-10-01|2011-10-30' => { :op => '><', :values => ['2011-10-01', '2011-10-30'] },
177 177 '<t+2' => { :op => '<t+', :values => ['2'] },
178 178 '>t+2' => { :op => '>t+', :values => ['2'] },
179 179 't+2' => { :op => 't+', :values => ['2'] },
180 180 't' => { :op => 't', :values => [''] },
181 181 'w' => { :op => 'w', :values => [''] },
182 182 '>t-2' => { :op => '>t-', :values => ['2'] },
183 183 '<t-2' => { :op => '<t-', :values => ['2'] },
184 184 't-2' => { :op => 't-', :values => ['2'] }},
185 185 'created_on' => {
186 186 '>=2011-10-12' => { :op => '>=', :values => ['2011-10-12'] },
187 187 '<t-2' => { :op => '<t-', :values => ['2'] },
188 188 '>t-2' => { :op => '>t-', :values => ['2'] },
189 189 't-2' => { :op => 't-', :values => ['2'] }},
190 190 'cf_1' => {
191 191 'c' => { :op => '=', :values => ['c'] },
192 192 '!c' => { :op => '!', :values => ['c'] },
193 193 '!*' => { :op => '!*', :values => [''] },
194 194 '*' => { :op => '*', :values => [''] }},
195 195 'estimated_hours' => {
196 196 '=13.4' => { :op => '=', :values => ['13.4'] },
197 197 '>=45' => { :op => '>=', :values => ['45'] },
198 198 '<=125' => { :op => '<=', :values => ['125'] },
199 199 '><10.5|20.5' => { :op => '><', :values => ['10.5', '20.5'] },
200 200 '!*' => { :op => '!*', :values => [''] },
201 201 '*' => { :op => '*', :values => [''] }}
202 202 }
203 203
204 204 default_filter = { 'status_id' => {:operator => 'o', :values => [''] }}
205 205
206 206 to_test.each do |field, expression_and_expected|
207 207 expression_and_expected.each do |filter_expression, expected|
208 208
209 209 get :index, :set_filter => 1, field => filter_expression
210 210
211 211 assert_response :success
212 212 assert_template 'index'
213 213 assert_not_nil assigns(:issues)
214 214
215 215 query = assigns(:query)
216 216 assert_not_nil query
217 217 assert query.has_filter?(field)
218 218 assert_equal(default_filter.merge({field => {:operator => expected[:op], :values => expected[:values]}}), query.filters)
219 219 end
220 220 end
221 221 end
222 222
223 223 def test_index_with_project_and_empty_filters
224 224 get :index, :project_id => 1, :set_filter => 1, :fields => ['']
225 225 assert_response :success
226 226 assert_template 'index'
227 227 assert_not_nil assigns(:issues)
228 228
229 229 query = assigns(:query)
230 230 assert_not_nil query
231 231 # no filter
232 232 assert_equal({}, query.filters)
233 233 end
234 234
235 235 def test_index_with_project_custom_field_filter
236 236 field = ProjectCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
237 237 CustomValue.create!(:custom_field => field, :customized => Project.find(3), :value => 'Foo')
238 238 CustomValue.create!(:custom_field => field, :customized => Project.find(5), :value => 'Foo')
239 239 filter_name = "project.cf_#{field.id}"
240 240 @request.session[:user_id] = 1
241 241
242 242 get :index, :set_filter => 1,
243 243 :f => [filter_name],
244 244 :op => {filter_name => '='},
245 245 :v => {filter_name => ['Foo']}
246 246 assert_response :success
247 247 assert_template 'index'
248 248 assert_equal [3, 5], assigns(:issues).map(&:project_id).uniq.sort
249 249 end
250 250
251 251 def test_index_with_query
252 252 get :index, :project_id => 1, :query_id => 5
253 253 assert_response :success
254 254 assert_template 'index'
255 255 assert_not_nil assigns(:issues)
256 256 assert_nil assigns(:issue_count_by_group)
257 257 end
258 258
259 259 def test_index_with_query_grouped_by_tracker
260 260 get :index, :project_id => 1, :query_id => 6
261 261 assert_response :success
262 262 assert_template 'index'
263 263 assert_not_nil assigns(:issues)
264 264 assert_not_nil assigns(:issue_count_by_group)
265 265 end
266 266
267 267 def test_index_with_query_grouped_by_list_custom_field
268 268 get :index, :project_id => 1, :query_id => 9
269 269 assert_response :success
270 270 assert_template 'index'
271 271 assert_not_nil assigns(:issues)
272 272 assert_not_nil assigns(:issue_count_by_group)
273 273 end
274 274
275 275 def test_index_with_query_grouped_by_user_custom_field
276 276 cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1,2,3], :field_format => 'user')
277 277 CustomValue.create!(:custom_field => cf, :customized => Issue.find(1), :value => '2')
278 278 CustomValue.create!(:custom_field => cf, :customized => Issue.find(2), :value => '3')
279 279 CustomValue.create!(:custom_field => cf, :customized => Issue.find(3), :value => '3')
280 280 CustomValue.create!(:custom_field => cf, :customized => Issue.find(5), :value => '')
281 281
282 282 get :index, :project_id => 1, :set_filter => 1, :group_by => "cf_#{cf.id}"
283 283 assert_response :success
284 284
285 285 assert_select 'tr.group', 3
286 286 assert_select 'tr.group' do
287 287 assert_select 'a', :text => 'John Smith'
288 288 assert_select 'span.count', :text => '1'
289 289 end
290 290 assert_select 'tr.group' do
291 291 assert_select 'a', :text => 'Dave Lopper'
292 292 assert_select 'span.count', :text => '2'
293 293 end
294 294 end
295 295
296 296 def test_index_with_query_grouped_by_tracker_in_normal_order
297 297 3.times {|i| Issue.generate!(:tracker_id => (i + 1))}
298 298
299 299 get :index, :set_filter => 1, :group_by => 'tracker', :sort => 'id:desc'
300 300 assert_response :success
301 301
302 302 trackers = assigns(:issues).map(&:tracker).uniq
303 303 assert_equal [1, 2, 3], trackers.map(&:id)
304 304 end
305 305
306 306 def test_index_with_query_grouped_by_tracker_in_reverse_order
307 307 3.times {|i| Issue.generate!(:tracker_id => (i + 1))}
308 308
309 309 get :index, :set_filter => 1, :group_by => 'tracker', :sort => 'id:desc,tracker:desc'
310 310 assert_response :success
311 311
312 312 trackers = assigns(:issues).map(&:tracker).uniq
313 313 assert_equal [3, 2, 1], trackers.map(&:id)
314 314 end
315 315
316 316 def test_index_with_query_id_and_project_id_should_set_session_query
317 317 get :index, :project_id => 1, :query_id => 4
318 318 assert_response :success
319 319 assert_kind_of Hash, session[:query]
320 320 assert_equal 4, session[:query][:id]
321 321 assert_equal 1, session[:query][:project_id]
322 322 end
323 323
324 324 def test_index_with_invalid_query_id_should_respond_404
325 325 get :index, :project_id => 1, :query_id => 999
326 326 assert_response 404
327 327 end
328 328
329 329 def test_index_with_cross_project_query_in_session_should_show_project_issues
330 330 q = IssueQuery.create!(:name => "test", :user_id => 2, :visibility => IssueQuery::VISIBILITY_PRIVATE, :project => nil)
331 331 @request.session[:query] = {:id => q.id, :project_id => 1}
332 332
333 333 with_settings :display_subprojects_issues => '0' do
334 334 get :index, :project_id => 1
335 335 end
336 336 assert_response :success
337 337 assert_not_nil assigns(:query)
338 338 assert_equal q.id, assigns(:query).id
339 339 assert_equal 1, assigns(:query).project_id
340 340 assert_equal [1], assigns(:issues).map(&:project_id).uniq
341 341 end
342 342
343 343 def test_private_query_should_not_be_available_to_other_users
344 344 q = IssueQuery.create!(:name => "private", :user => User.find(2), :visibility => IssueQuery::VISIBILITY_PRIVATE, :project => nil)
345 345 @request.session[:user_id] = 3
346 346
347 347 get :index, :query_id => q.id
348 348 assert_response 403
349 349 end
350 350
351 351 def test_private_query_should_be_available_to_its_user
352 352 q = IssueQuery.create!(:name => "private", :user => User.find(2), :visibility => IssueQuery::VISIBILITY_PRIVATE, :project => nil)
353 353 @request.session[:user_id] = 2
354 354
355 355 get :index, :query_id => q.id
356 356 assert_response :success
357 357 end
358 358
359 359 def test_public_query_should_be_available_to_other_users
360 360 q = IssueQuery.create!(:name => "private", :user => User.find(2), :visibility => IssueQuery::VISIBILITY_PUBLIC, :project => nil)
361 361 @request.session[:user_id] = 3
362 362
363 363 get :index, :query_id => q.id
364 364 assert_response :success
365 365 end
366 366
367 367 def test_index_should_omit_page_param_in_export_links
368 368 get :index, :page => 2
369 369 assert_response :success
370 370 assert_select 'a.atom[href=/issues.atom]'
371 371 assert_select 'a.csv[href=/issues.csv]'
372 372 assert_select 'a.pdf[href=/issues.pdf]'
373 373 assert_select 'form#csv-export-form[action=/issues.csv]'
374 374 end
375 375
376 376 def test_index_should_not_warn_when_not_exceeding_export_limit
377 377 with_settings :issues_export_limit => 200 do
378 378 get :index
379 379 assert_select '#csv-export-options p.icon-warning', 0
380 380 end
381 381 end
382 382
383 383 def test_index_should_warn_when_exceeding_export_limit
384 384 with_settings :issues_export_limit => 2 do
385 385 get :index
386 386 assert_select '#csv-export-options p.icon-warning', :text => %r{limit: 2}
387 387 end
388 388 end
389 389
390 390 def test_index_csv
391 391 get :index, :format => 'csv'
392 392 assert_response :success
393 393 assert_not_nil assigns(:issues)
394 394 assert_equal 'text/csv; header=present', @response.content_type
395 395 assert @response.body.starts_with?("#,")
396 396 lines = @response.body.chomp.split("\n")
397 397 assert_equal assigns(:query).columns.size, lines[0].split(',').size
398 398 end
399 399
400 400 def test_index_csv_with_project
401 401 get :index, :project_id => 1, :format => 'csv'
402 402 assert_response :success
403 403 assert_not_nil assigns(:issues)
404 404 assert_equal 'text/csv; header=present', @response.content_type
405 405 end
406 406
407 407 def test_index_csv_with_description
408 408 Issue.generate!(:description => 'test_index_csv_with_description')
409 409
410 410 with_settings :default_language => 'en' do
411 411 get :index, :format => 'csv', :description => '1'
412 412 assert_response :success
413 413 assert_not_nil assigns(:issues)
414 414 end
415 415
416 416 assert_equal 'text/csv; header=present', response.content_type
417 417 headers = response.body.chomp.split("\n").first.split(',')
418 418 assert_include 'Description', headers
419 419 assert_include 'test_index_csv_with_description', response.body
420 420 end
421 421
422 422 def test_index_csv_with_spent_time_column
423 423 issue = Issue.create!(:project_id => 1, :tracker_id => 1, :subject => 'test_index_csv_with_spent_time_column', :author_id => 2)
424 424 TimeEntry.create!(:project => issue.project, :issue => issue, :hours => 7.33, :user => User.find(2), :spent_on => Date.today)
425 425
426 426 get :index, :format => 'csv', :set_filter => '1', :c => %w(subject spent_hours)
427 427 assert_response :success
428 428 assert_equal 'text/csv; header=present', @response.content_type
429 429 lines = @response.body.chomp.split("\n")
430 430 assert_include "#{issue.id},#{issue.subject},7.33", lines
431 431 end
432 432
433 433 def test_index_csv_with_all_columns
434 434 get :index, :format => 'csv', :columns => 'all'
435 435 assert_response :success
436 436 assert_not_nil assigns(:issues)
437 437 assert_equal 'text/csv; header=present', @response.content_type
438 438 assert_match /\A#,/, response.body
439 439 lines = response.body.chomp.split("\n")
440 440 assert_equal assigns(:query).available_inline_columns.size, lines[0].split(',').size
441 441 end
442 442
443 443 def test_index_csv_with_multi_column_field
444 444 CustomField.find(1).update_attribute :multiple, true
445 445 issue = Issue.find(1)
446 446 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
447 447 issue.save!
448 448
449 449 get :index, :format => 'csv', :columns => 'all'
450 450 assert_response :success
451 451 lines = @response.body.chomp.split("\n")
452 452 assert lines.detect {|line| line.include?('"MySQL, Oracle"')}
453 453 end
454 454
455 455 def test_index_csv_should_format_float_custom_fields_with_csv_decimal_separator
456 456 field = IssueCustomField.create!(:name => 'Float', :is_for_all => true, :tracker_ids => [1], :field_format => 'float')
457 457 issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {field.id => '185.6'})
458 458
459 459 with_settings :default_language => 'fr' do
460 460 get :index, :format => 'csv', :columns => 'all'
461 461 assert_response :success
462 462 issue_line = response.body.chomp.split("\n").map {|line| line.split(';')}.detect {|line| line[0]==issue.id.to_s}
463 463 assert_include '185,60', issue_line
464 464 end
465 465
466 466 with_settings :default_language => 'en' do
467 467 get :index, :format => 'csv', :columns => 'all'
468 468 assert_response :success
469 469 issue_line = response.body.chomp.split("\n").map {|line| line.split(',')}.detect {|line| line[0]==issue.id.to_s}
470 470 assert_include '185.60', issue_line
471 471 end
472 472 end
473 473
474 474 def test_index_csv_big_5
475 475 with_settings :default_language => "zh-TW" do
476 476 str_utf8 = "\xe4\xb8\x80\xe6\x9c\x88"
477 477 str_big5 = "\xa4@\xa4\xeb"
478 478 if str_utf8.respond_to?(:force_encoding)
479 479 str_utf8.force_encoding('UTF-8')
480 480 str_big5.force_encoding('Big5')
481 481 end
482 482 issue = Issue.generate!(:subject => str_utf8)
483 483
484 484 get :index, :project_id => 1,
485 485 :f => ['subject'],
486 486 :op => '=', :values => [str_utf8],
487 487 :format => 'csv'
488 488 assert_equal 'text/csv; header=present', @response.content_type
489 489 lines = @response.body.chomp.split("\n")
490 490 s1 = "\xaa\xac\xbaA"
491 491 if str_utf8.respond_to?(:force_encoding)
492 492 s1.force_encoding('Big5')
493 493 end
494 494 assert_include s1, lines[0]
495 495 assert_include str_big5, lines[1]
496 496 end
497 497 end
498 498
499 499 def test_index_csv_cannot_convert_should_be_replaced_big_5
500 500 with_settings :default_language => "zh-TW" do
501 501 str_utf8 = "\xe4\xbb\xa5\xe5\x86\x85"
502 502 if str_utf8.respond_to?(:force_encoding)
503 503 str_utf8.force_encoding('UTF-8')
504 504 end
505 505 issue = Issue.generate!(:subject => str_utf8)
506 506
507 507 get :index, :project_id => 1,
508 508 :f => ['subject'],
509 509 :op => '=', :values => [str_utf8],
510 510 :c => ['status', 'subject'],
511 511 :format => 'csv',
512 512 :set_filter => 1
513 513 assert_equal 'text/csv; header=present', @response.content_type
514 514 lines = @response.body.chomp.split("\n")
515 515 s1 = "\xaa\xac\xbaA" # status
516 516 if str_utf8.respond_to?(:force_encoding)
517 517 s1.force_encoding('Big5')
518 518 end
519 519 assert lines[0].include?(s1)
520 520 s2 = lines[1].split(",")[2]
521 521 if s1.respond_to?(:force_encoding)
522 522 s3 = "\xa5H?" # subject
523 523 s3.force_encoding('Big5')
524 524 assert_equal s3, s2
525 525 elsif RUBY_PLATFORM == 'java'
526 526 assert_equal "??", s2
527 527 else
528 528 assert_equal "\xa5H???", s2
529 529 end
530 530 end
531 531 end
532 532
533 533 def test_index_csv_tw
534 534 with_settings :default_language => "zh-TW" do
535 535 str1 = "test_index_csv_tw"
536 536 issue = Issue.generate!(:subject => str1, :estimated_hours => '1234.5')
537 537
538 538 get :index, :project_id => 1,
539 539 :f => ['subject'],
540 540 :op => '=', :values => [str1],
541 541 :c => ['estimated_hours', 'subject'],
542 542 :format => 'csv',
543 543 :set_filter => 1
544 544 assert_equal 'text/csv; header=present', @response.content_type
545 545 lines = @response.body.chomp.split("\n")
546 546 assert_equal "#{issue.id},1234.50,#{str1}", lines[1]
547 547 end
548 548 end
549 549
550 550 def test_index_csv_fr
551 551 with_settings :default_language => "fr" do
552 552 str1 = "test_index_csv_fr"
553 553 issue = Issue.generate!(:subject => str1, :estimated_hours => '1234.5')
554 554
555 555 get :index, :project_id => 1,
556 556 :f => ['subject'],
557 557 :op => '=', :values => [str1],
558 558 :c => ['estimated_hours', 'subject'],
559 559 :format => 'csv',
560 560 :set_filter => 1
561 561 assert_equal 'text/csv; header=present', @response.content_type
562 562 lines = @response.body.chomp.split("\n")
563 563 assert_equal "#{issue.id};1234,50;#{str1}", lines[1]
564 564 end
565 565 end
566 566
567 567 def test_index_pdf
568 568 ["en", "zh", "zh-TW", "ja", "ko"].each do |lang|
569 569 with_settings :default_language => lang do
570 570
571 571 get :index
572 572 assert_response :success
573 573 assert_template 'index'
574 574
575 575 if lang == "ja"
576 576 if RUBY_PLATFORM != 'java'
577 577 assert_equal "CP932", l(:general_pdf_encoding)
578 578 end
579 579 if RUBY_PLATFORM == 'java' && l(:general_pdf_encoding) == "CP932"
580 580 next
581 581 end
582 582 end
583 583
584 584 get :index, :format => 'pdf'
585 585 assert_response :success
586 586 assert_not_nil assigns(:issues)
587 587 assert_equal 'application/pdf', @response.content_type
588 588
589 589 get :index, :project_id => 1, :format => 'pdf'
590 590 assert_response :success
591 591 assert_not_nil assigns(:issues)
592 592 assert_equal 'application/pdf', @response.content_type
593 593
594 594 get :index, :project_id => 1, :query_id => 6, :format => 'pdf'
595 595 assert_response :success
596 596 assert_not_nil assigns(:issues)
597 597 assert_equal 'application/pdf', @response.content_type
598 598 end
599 599 end
600 600 end
601 601
602 602 def test_index_pdf_with_query_grouped_by_list_custom_field
603 603 get :index, :project_id => 1, :query_id => 9, :format => 'pdf'
604 604 assert_response :success
605 605 assert_not_nil assigns(:issues)
606 606 assert_not_nil assigns(:issue_count_by_group)
607 607 assert_equal 'application/pdf', @response.content_type
608 608 end
609 609
610 610 def test_index_atom
611 611 get :index, :project_id => 'ecookbook', :format => 'atom'
612 612 assert_response :success
613 613 assert_template 'common/feed'
614 614 assert_equal 'application/atom+xml', response.content_type
615 615
616 616 assert_select 'feed' do
617 617 assert_select 'link[rel=self][href=?]', 'http://test.host/projects/ecookbook/issues.atom'
618 618 assert_select 'link[rel=alternate][href=?]', 'http://test.host/projects/ecookbook/issues'
619 619 assert_select 'entry link[href=?]', 'http://test.host/issues/1'
620 620 end
621 621 end
622 622
623 623 def test_index_sort
624 624 get :index, :sort => 'tracker,id:desc'
625 625 assert_response :success
626 626
627 627 sort_params = @request.session['issues_index_sort']
628 628 assert sort_params.is_a?(String)
629 629 assert_equal 'tracker,id:desc', sort_params
630 630
631 631 issues = assigns(:issues)
632 632 assert_not_nil issues
633 633 assert !issues.empty?
634 634 assert_equal issues.sort {|a,b| a.tracker == b.tracker ? b.id <=> a.id : a.tracker <=> b.tracker }.collect(&:id), issues.collect(&:id)
635 635 end
636 636
637 637 def test_index_sort_by_field_not_included_in_columns
638 638 Setting.issue_list_default_columns = %w(subject author)
639 639 get :index, :sort => 'tracker'
640 640 end
641 641
642 642 def test_index_sort_by_assigned_to
643 643 get :index, :sort => 'assigned_to'
644 644 assert_response :success
645 645 assignees = assigns(:issues).collect(&:assigned_to).compact
646 646 assert_equal assignees.sort, assignees
647 647 end
648 648
649 649 def test_index_sort_by_assigned_to_desc
650 650 get :index, :sort => 'assigned_to:desc'
651 651 assert_response :success
652 652 assignees = assigns(:issues).collect(&:assigned_to).compact
653 653 assert_equal assignees.sort.reverse, assignees
654 654 end
655 655
656 656 def test_index_group_by_assigned_to
657 657 get :index, :group_by => 'assigned_to', :sort => 'priority'
658 658 assert_response :success
659 659 end
660 660
661 661 def test_index_sort_by_author
662 662 get :index, :sort => 'author'
663 663 assert_response :success
664 664 authors = assigns(:issues).collect(&:author)
665 665 assert_equal authors.sort, authors
666 666 end
667 667
668 668 def test_index_sort_by_author_desc
669 669 get :index, :sort => 'author:desc'
670 670 assert_response :success
671 671 authors = assigns(:issues).collect(&:author)
672 672 assert_equal authors.sort.reverse, authors
673 673 end
674 674
675 675 def test_index_group_by_author
676 676 get :index, :group_by => 'author', :sort => 'priority'
677 677 assert_response :success
678 678 end
679 679
680 680 def test_index_sort_by_spent_hours
681 681 get :index, :sort => 'spent_hours:desc'
682 682 assert_response :success
683 683 hours = assigns(:issues).collect(&:spent_hours)
684 684 assert_equal hours.sort.reverse, hours
685 685 end
686 686
687 687 def test_index_sort_by_user_custom_field
688 688 cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1,2,3], :field_format => 'user')
689 689 CustomValue.create!(:custom_field => cf, :customized => Issue.find(1), :value => '2')
690 690 CustomValue.create!(:custom_field => cf, :customized => Issue.find(2), :value => '3')
691 691 CustomValue.create!(:custom_field => cf, :customized => Issue.find(3), :value => '3')
692 692 CustomValue.create!(:custom_field => cf, :customized => Issue.find(5), :value => '')
693 693
694 694 get :index, :project_id => 1, :set_filter => 1, :sort => "cf_#{cf.id},id"
695 695 assert_response :success
696 696
697 697 assert_equal [2, 3, 1], assigns(:issues).select {|issue| issue.custom_field_value(cf).present?}.map(&:id)
698 698 end
699 699
700 700 def test_index_with_columns
701 701 columns = ['tracker', 'subject', 'assigned_to']
702 702 get :index, :set_filter => 1, :c => columns
703 703 assert_response :success
704 704
705 705 # query should use specified columns
706 706 query = assigns(:query)
707 707 assert_kind_of IssueQuery, query
708 708 assert_equal columns, query.column_names.map(&:to_s)
709 709
710 710 # columns should be stored in session
711 711 assert_kind_of Hash, session[:query]
712 712 assert_kind_of Array, session[:query][:column_names]
713 713 assert_equal columns, session[:query][:column_names].map(&:to_s)
714 714
715 715 # ensure only these columns are kept in the selected columns list
716 716 assert_select 'select#selected_columns option' do
717 717 assert_select 'option', 3
718 718 assert_select 'option[value=tracker]'
719 719 assert_select 'option[value=project]', 0
720 720 end
721 721 end
722 722
723 723 def test_index_without_project_should_implicitly_add_project_column_to_default_columns
724 724 Setting.issue_list_default_columns = ['tracker', 'subject', 'assigned_to']
725 725 get :index, :set_filter => 1
726 726
727 727 # query should use specified columns
728 728 query = assigns(:query)
729 729 assert_kind_of IssueQuery, query
730 730 assert_equal [:id, :project, :tracker, :subject, :assigned_to], query.columns.map(&:name)
731 731 end
732 732
733 733 def test_index_without_project_and_explicit_default_columns_should_not_add_project_column
734 734 Setting.issue_list_default_columns = ['tracker', 'subject', 'assigned_to']
735 735 columns = ['id', 'tracker', 'subject', 'assigned_to']
736 736 get :index, :set_filter => 1, :c => columns
737 737
738 738 # query should use specified columns
739 739 query = assigns(:query)
740 740 assert_kind_of IssueQuery, query
741 741 assert_equal columns.map(&:to_sym), query.columns.map(&:name)
742 742 end
743 743
744 744 def test_index_with_custom_field_column
745 745 columns = %w(tracker subject cf_2)
746 746 get :index, :set_filter => 1, :c => columns
747 747 assert_response :success
748 748
749 749 # query should use specified columns
750 750 query = assigns(:query)
751 751 assert_kind_of IssueQuery, query
752 752 assert_equal columns, query.column_names.map(&:to_s)
753 753
754 754 assert_select 'table.issues td.cf_2.string'
755 755 end
756 756
757 757 def test_index_with_multi_custom_field_column
758 758 field = CustomField.find(1)
759 759 field.update_attribute :multiple, true
760 760 issue = Issue.find(1)
761 761 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
762 762 issue.save!
763 763
764 764 get :index, :set_filter => 1, :c => %w(tracker subject cf_1)
765 765 assert_response :success
766 766
767 767 assert_select 'table.issues td.cf_1', :text => 'MySQL, Oracle'
768 768 end
769 769
770 770 def test_index_with_multi_user_custom_field_column
771 771 field = IssueCustomField.create!(:name => 'Multi user', :field_format => 'user', :multiple => true,
772 772 :tracker_ids => [1], :is_for_all => true)
773 773 issue = Issue.find(1)
774 774 issue.custom_field_values = {field.id => ['2', '3']}
775 775 issue.save!
776 776
777 777 get :index, :set_filter => 1, :c => ['tracker', 'subject', "cf_#{field.id}"]
778 778 assert_response :success
779 779
780 780 assert_select "table.issues td.cf_#{field.id}" do
781 781 assert_select 'a', 2
782 782 assert_select 'a[href=?]', '/users/2', :text => 'John Smith'
783 783 assert_select 'a[href=?]', '/users/3', :text => 'Dave Lopper'
784 784 end
785 785 end
786 786
787 787 def test_index_with_date_column
788 788 with_settings :date_format => '%d/%m/%Y' do
789 789 Issue.find(1).update_attribute :start_date, '1987-08-24'
790 790 get :index, :set_filter => 1, :c => %w(start_date)
791 791 assert_select "table.issues td.start_date", :text => '24/08/1987'
792 792 end
793 793 end
794 794
795 795 def test_index_with_done_ratio_column
796 796 Issue.find(1).update_attribute :done_ratio, 40
797 797 get :index, :set_filter => 1, :c => %w(done_ratio)
798 798 assert_select 'table.issues td.done_ratio' do
799 799 assert_select 'table.progress' do
800 800 assert_select 'td.closed[style=?]', 'width: 40%;'
801 801 end
802 802 end
803 803 end
804 804
805 805 def test_index_with_spent_hours_column
806 806 get :index, :set_filter => 1, :c => %w(subject spent_hours)
807 807 assert_select 'table.issues tr#issue-3 td.spent_hours', :text => '1.00'
808 808 end
809 809
810 810 def test_index_should_not_show_spent_hours_column_without_permission
811 811 Role.anonymous.remove_permission! :view_time_entries
812 812 get :index, :set_filter => 1, :c => %w(subject spent_hours)
813 813 assert_select 'td.spent_hours', 0
814 814 end
815 815
816 816 def test_index_with_fixed_version_column
817 817 get :index, :set_filter => 1, :c => %w(fixed_version)
818 818 assert_select 'table.issues td.fixed_version' do
819 assert_select 'a[href=?]', '/versions/2', :text => 'eCookbook - 1.0'
819 assert_select 'a[href=?]', '/versions/2', :text => '1.0'
820 820 end
821 821 end
822 822
823 823 def test_index_with_relations_column
824 824 IssueRelation.delete_all
825 825 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(7))
826 826 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(8), :issue_to => Issue.find(1))
827 827 IssueRelation.create!(:relation_type => "blocks", :issue_from => Issue.find(1), :issue_to => Issue.find(11))
828 828 IssueRelation.create!(:relation_type => "blocks", :issue_from => Issue.find(12), :issue_to => Issue.find(2))
829 829
830 830 get :index, :set_filter => 1, :c => %w(subject relations)
831 831 assert_response :success
832 832 assert_select "tr#issue-1 td.relations" do
833 833 assert_select "span", 3
834 834 assert_select "span", :text => "Related to #7"
835 835 assert_select "span", :text => "Related to #8"
836 836 assert_select "span", :text => "Blocks #11"
837 837 end
838 838 assert_select "tr#issue-2 td.relations" do
839 839 assert_select "span", 1
840 840 assert_select "span", :text => "Blocked by #12"
841 841 end
842 842 assert_select "tr#issue-3 td.relations" do
843 843 assert_select "span", 0
844 844 end
845 845
846 846 get :index, :set_filter => 1, :c => %w(relations), :format => 'csv'
847 847 assert_response :success
848 848 assert_equal 'text/csv; header=present', response.content_type
849 849 lines = response.body.chomp.split("\n")
850 850 assert_include '1,"Related to #7, Related to #8, Blocks #11"', lines
851 851 assert_include '2,Blocked by #12', lines
852 852 assert_include '3,""', lines
853 853
854 854 get :index, :set_filter => 1, :c => %w(subject relations), :format => 'pdf'
855 855 assert_response :success
856 856 assert_equal 'application/pdf', response.content_type
857 857 end
858 858
859 859 def test_index_with_description_column
860 860 get :index, :set_filter => 1, :c => %w(subject description)
861 861
862 862 assert_select 'table.issues thead th', 3 # columns: chekbox + id + subject
863 863 assert_select 'td.description[colspan=3]', :text => 'Unable to print recipes'
864 864
865 865 get :index, :set_filter => 1, :c => %w(subject description), :format => 'pdf'
866 866 assert_response :success
867 867 assert_equal 'application/pdf', response.content_type
868 868 end
869 869
870 870 def test_index_send_html_if_query_is_invalid
871 871 get :index, :f => ['start_date'], :op => {:start_date => '='}
872 872 assert_equal 'text/html', @response.content_type
873 873 assert_template 'index'
874 874 end
875 875
876 876 def test_index_send_nothing_if_query_is_invalid
877 877 get :index, :f => ['start_date'], :op => {:start_date => '='}, :format => 'csv'
878 878 assert_equal 'text/csv', @response.content_type
879 879 assert @response.body.blank?
880 880 end
881 881
882 882 def test_show_by_anonymous
883 883 get :show, :id => 1
884 884 assert_response :success
885 885 assert_template 'show'
886 886 assert_equal Issue.find(1), assigns(:issue)
887 887 assert_select 'div.issue div.description', :text => /Unable to print recipes/
888 888 # anonymous role is allowed to add a note
889 889 assert_select 'form#issue-form' do
890 890 assert_select 'fieldset' do
891 891 assert_select 'legend', :text => 'Notes'
892 892 assert_select 'textarea[name=?]', 'issue[notes]'
893 893 end
894 894 end
895 895 assert_select 'title', :text => "Bug #1: #{ESCAPED_UCANT} print recipes - eCookbook - Redmine"
896 896 end
897 897
898 898 def test_show_by_manager
899 899 @request.session[:user_id] = 2
900 900 get :show, :id => 1
901 901 assert_response :success
902 902 assert_select 'a', :text => /Quote/
903 903 assert_select 'form#issue-form' do
904 904 assert_select 'fieldset' do
905 905 assert_select 'legend', :text => 'Change properties'
906 906 assert_select 'input[name=?]', 'issue[subject]'
907 907 end
908 908 assert_select 'fieldset' do
909 909 assert_select 'legend', :text => 'Log time'
910 910 assert_select 'input[name=?]', 'time_entry[hours]'
911 911 end
912 912 assert_select 'fieldset' do
913 913 assert_select 'legend', :text => 'Notes'
914 914 assert_select 'textarea[name=?]', 'issue[notes]'
915 915 end
916 916 end
917 917 end
918 918
919 919 def test_show_should_display_update_form
920 920 @request.session[:user_id] = 2
921 921 get :show, :id => 1
922 922 assert_response :success
923 923
924 924 assert_select 'form#issue-form' do
925 925 assert_select 'input[name=?]', 'issue[is_private]'
926 926 assert_select 'select[name=?]', 'issue[project_id]'
927 927 assert_select 'select[name=?]', 'issue[tracker_id]'
928 928 assert_select 'input[name=?]', 'issue[subject]'
929 929 assert_select 'textarea[name=?]', 'issue[description]'
930 930 assert_select 'select[name=?]', 'issue[status_id]'
931 931 assert_select 'select[name=?]', 'issue[priority_id]'
932 932 assert_select 'select[name=?]', 'issue[assigned_to_id]'
933 933 assert_select 'select[name=?]', 'issue[category_id]'
934 934 assert_select 'select[name=?]', 'issue[fixed_version_id]'
935 935 assert_select 'input[name=?]', 'issue[parent_issue_id]'
936 936 assert_select 'input[name=?]', 'issue[start_date]'
937 937 assert_select 'input[name=?]', 'issue[due_date]'
938 938 assert_select 'select[name=?]', 'issue[done_ratio]'
939 939 assert_select 'input[name=?]', 'issue[custom_field_values][2]'
940 940 assert_select 'input[name=?]', 'issue[watcher_user_ids][]', 0
941 941 assert_select 'textarea[name=?]', 'issue[notes]'
942 942 end
943 943 end
944 944
945 945 def test_show_should_display_update_form_with_minimal_permissions
946 946 Role.find(1).update_attribute :permissions, [:view_issues, :add_issue_notes]
947 947 WorkflowTransition.delete_all :role_id => 1
948 948
949 949 @request.session[:user_id] = 2
950 950 get :show, :id => 1
951 951 assert_response :success
952 952
953 953 assert_select 'form#issue-form' do
954 954 assert_select 'input[name=?]', 'issue[is_private]', 0
955 955 assert_select 'select[name=?]', 'issue[project_id]', 0
956 956 assert_select 'select[name=?]', 'issue[tracker_id]', 0
957 957 assert_select 'input[name=?]', 'issue[subject]', 0
958 958 assert_select 'textarea[name=?]', 'issue[description]', 0
959 959 assert_select 'select[name=?]', 'issue[status_id]', 0
960 960 assert_select 'select[name=?]', 'issue[priority_id]', 0
961 961 assert_select 'select[name=?]', 'issue[assigned_to_id]', 0
962 962 assert_select 'select[name=?]', 'issue[category_id]', 0
963 963 assert_select 'select[name=?]', 'issue[fixed_version_id]', 0
964 964 assert_select 'input[name=?]', 'issue[parent_issue_id]', 0
965 965 assert_select 'input[name=?]', 'issue[start_date]', 0
966 966 assert_select 'input[name=?]', 'issue[due_date]', 0
967 967 assert_select 'select[name=?]', 'issue[done_ratio]', 0
968 968 assert_select 'input[name=?]', 'issue[custom_field_values][2]', 0
969 969 assert_select 'input[name=?]', 'issue[watcher_user_ids][]', 0
970 970 assert_select 'textarea[name=?]', 'issue[notes]'
971 971 end
972 972 end
973 973
974 974 def test_show_should_display_update_form_with_workflow_permissions
975 975 Role.find(1).update_attribute :permissions, [:view_issues, :add_issue_notes]
976 976
977 977 @request.session[:user_id] = 2
978 978 get :show, :id => 1
979 979 assert_response :success
980 980
981 981 assert_select 'form#issue-form' do
982 982 assert_select 'input[name=?]', 'issue[is_private]', 0
983 983 assert_select 'select[name=?]', 'issue[project_id]', 0
984 984 assert_select 'select[name=?]', 'issue[tracker_id]', 0
985 985 assert_select 'input[name=?]', 'issue[subject]', 0
986 986 assert_select 'textarea[name=?]', 'issue[description]', 0
987 987 assert_select 'select[name=?]', 'issue[status_id]'
988 988 assert_select 'select[name=?]', 'issue[priority_id]', 0
989 989 assert_select 'select[name=?]', 'issue[assigned_to_id]'
990 990 assert_select 'select[name=?]', 'issue[category_id]', 0
991 991 assert_select 'select[name=?]', 'issue[fixed_version_id]'
992 992 assert_select 'input[name=?]', 'issue[parent_issue_id]', 0
993 993 assert_select 'input[name=?]', 'issue[start_date]', 0
994 994 assert_select 'input[name=?]', 'issue[due_date]', 0
995 995 assert_select 'select[name=?]', 'issue[done_ratio]'
996 996 assert_select 'input[name=?]', 'issue[custom_field_values][2]', 0
997 997 assert_select 'input[name=?]', 'issue[watcher_user_ids][]', 0
998 998 assert_select 'textarea[name=?]', 'issue[notes]'
999 999 end
1000 1000 end
1001 1001
1002 1002 def test_show_should_not_display_update_form_without_permissions
1003 1003 Role.find(1).update_attribute :permissions, [:view_issues]
1004 1004
1005 1005 @request.session[:user_id] = 2
1006 1006 get :show, :id => 1
1007 1007 assert_response :success
1008 1008
1009 1009 assert_select 'form#issue-form', 0
1010 1010 end
1011 1011
1012 1012 def test_update_form_should_not_display_inactive_enumerations
1013 1013 assert !IssuePriority.find(15).active?
1014 1014
1015 1015 @request.session[:user_id] = 2
1016 1016 get :show, :id => 1
1017 1017 assert_response :success
1018 1018
1019 1019 assert_select 'form#issue-form' do
1020 1020 assert_select 'select[name=?]', 'issue[priority_id]' do
1021 1021 assert_select 'option[value=4]'
1022 1022 assert_select 'option[value=15]', 0
1023 1023 end
1024 1024 end
1025 1025 end
1026 1026
1027 1027 def test_update_form_should_allow_attachment_upload
1028 1028 @request.session[:user_id] = 2
1029 1029 get :show, :id => 1
1030 1030
1031 1031 assert_select 'form#issue-form[method=post][enctype=multipart/form-data]' do
1032 1032 assert_select 'input[type=file][name=?]', 'attachments[dummy][file]'
1033 1033 end
1034 1034 end
1035 1035
1036 1036 def test_show_should_deny_anonymous_access_without_permission
1037 1037 Role.anonymous.remove_permission!(:view_issues)
1038 1038 get :show, :id => 1
1039 1039 assert_response :redirect
1040 1040 end
1041 1041
1042 1042 def test_show_should_deny_anonymous_access_to_private_issue
1043 1043 Issue.where(:id => 1).update_all(["is_private = ?", true])
1044 1044 get :show, :id => 1
1045 1045 assert_response :redirect
1046 1046 end
1047 1047
1048 1048 def test_show_should_deny_non_member_access_without_permission
1049 1049 Role.non_member.remove_permission!(:view_issues)
1050 1050 @request.session[:user_id] = 9
1051 1051 get :show, :id => 1
1052 1052 assert_response 403
1053 1053 end
1054 1054
1055 1055 def test_show_should_deny_non_member_access_to_private_issue
1056 1056 Issue.where(:id => 1).update_all(["is_private = ?", true])
1057 1057 @request.session[:user_id] = 9
1058 1058 get :show, :id => 1
1059 1059 assert_response 403
1060 1060 end
1061 1061
1062 1062 def test_show_should_deny_member_access_without_permission
1063 1063 Role.find(1).remove_permission!(:view_issues)
1064 1064 @request.session[:user_id] = 2
1065 1065 get :show, :id => 1
1066 1066 assert_response 403
1067 1067 end
1068 1068
1069 1069 def test_show_should_deny_member_access_to_private_issue_without_permission
1070 1070 Issue.where(:id => 1).update_all(["is_private = ?", true])
1071 1071 @request.session[:user_id] = 3
1072 1072 get :show, :id => 1
1073 1073 assert_response 403
1074 1074 end
1075 1075
1076 1076 def test_show_should_allow_author_access_to_private_issue
1077 1077 Issue.where(:id => 1).update_all(["is_private = ?, author_id = 3", true])
1078 1078 @request.session[:user_id] = 3
1079 1079 get :show, :id => 1
1080 1080 assert_response :success
1081 1081 end
1082 1082
1083 1083 def test_show_should_allow_assignee_access_to_private_issue
1084 1084 Issue.where(:id => 1).update_all(["is_private = ?, assigned_to_id = 3", true])
1085 1085 @request.session[:user_id] = 3
1086 1086 get :show, :id => 1
1087 1087 assert_response :success
1088 1088 end
1089 1089
1090 1090 def test_show_should_allow_member_access_to_private_issue_with_permission
1091 1091 Issue.where(:id => 1).update_all(["is_private = ?", true])
1092 1092 User.find(3).roles_for_project(Project.find(1)).first.update_attribute :issues_visibility, 'all'
1093 1093 @request.session[:user_id] = 3
1094 1094 get :show, :id => 1
1095 1095 assert_response :success
1096 1096 end
1097 1097
1098 1098 def test_show_should_not_disclose_relations_to_invisible_issues
1099 1099 Setting.cross_project_issue_relations = '1'
1100 1100 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
1101 1101 # Relation to a private project issue
1102 1102 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates')
1103 1103
1104 1104 get :show, :id => 1
1105 1105 assert_response :success
1106 1106
1107 1107 assert_select 'div#relations' do
1108 1108 assert_select 'a', :text => /#2$/
1109 1109 assert_select 'a', :text => /#4$/, :count => 0
1110 1110 end
1111 1111 end
1112 1112
1113 1113 def test_show_should_list_subtasks
1114 1114 Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :parent_issue_id => 1, :subject => 'Child Issue')
1115 1115
1116 1116 get :show, :id => 1
1117 1117 assert_response :success
1118 1118
1119 1119 assert_select 'div#issue_tree' do
1120 1120 assert_select 'td.subject', :text => /Child Issue/
1121 1121 end
1122 1122 end
1123 1123
1124 1124 def test_show_should_list_parents
1125 1125 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :parent_issue_id => 1, :subject => 'Child Issue')
1126 1126
1127 1127 get :show, :id => issue.id
1128 1128 assert_response :success
1129 1129
1130 1130 assert_select 'div.subject' do
1131 1131 assert_select 'h3', 'Child Issue'
1132 1132 assert_select 'a[href=/issues/1]'
1133 1133 end
1134 1134 end
1135 1135
1136 1136 def test_show_should_not_display_prev_next_links_without_query_in_session
1137 1137 get :show, :id => 1
1138 1138 assert_response :success
1139 1139 assert_nil assigns(:prev_issue_id)
1140 1140 assert_nil assigns(:next_issue_id)
1141 1141
1142 1142 assert_select 'div.next-prev-links', 0
1143 1143 end
1144 1144
1145 1145 def test_show_should_display_prev_next_links_with_query_in_session
1146 1146 @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'o'}}, :project_id => nil}
1147 1147 @request.session['issues_index_sort'] = 'id'
1148 1148
1149 1149 with_settings :display_subprojects_issues => '0' do
1150 1150 get :show, :id => 3
1151 1151 end
1152 1152
1153 1153 assert_response :success
1154 1154 # Previous and next issues for all projects
1155 1155 assert_equal 2, assigns(:prev_issue_id)
1156 1156 assert_equal 5, assigns(:next_issue_id)
1157 1157
1158 1158 count = Issue.open.visible.count
1159 1159
1160 1160 assert_select 'div.next-prev-links' do
1161 1161 assert_select 'a[href=/issues/2]', :text => /Previous/
1162 1162 assert_select 'a[href=/issues/5]', :text => /Next/
1163 1163 assert_select 'span.position', :text => "3 of #{count}"
1164 1164 end
1165 1165 end
1166 1166
1167 1167 def test_show_should_display_prev_next_links_with_saved_query_in_session
1168 1168 query = IssueQuery.create!(:name => 'test', :visibility => IssueQuery::VISIBILITY_PUBLIC, :user_id => 1,
1169 1169 :filters => {'status_id' => {:values => ['5'], :operator => '='}},
1170 1170 :sort_criteria => [['id', 'asc']])
1171 1171 @request.session[:query] = {:id => query.id, :project_id => nil}
1172 1172
1173 1173 get :show, :id => 11
1174 1174
1175 1175 assert_response :success
1176 1176 assert_equal query, assigns(:query)
1177 1177 # Previous and next issues for all projects
1178 1178 assert_equal 8, assigns(:prev_issue_id)
1179 1179 assert_equal 12, assigns(:next_issue_id)
1180 1180
1181 1181 assert_select 'div.next-prev-links' do
1182 1182 assert_select 'a[href=/issues/8]', :text => /Previous/
1183 1183 assert_select 'a[href=/issues/12]', :text => /Next/
1184 1184 end
1185 1185 end
1186 1186
1187 1187 def test_show_should_display_prev_next_links_with_query_and_sort_on_association
1188 1188 @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'o'}}, :project_id => nil}
1189 1189
1190 1190 %w(project tracker status priority author assigned_to category fixed_version).each do |assoc_sort|
1191 1191 @request.session['issues_index_sort'] = assoc_sort
1192 1192
1193 1193 get :show, :id => 3
1194 1194 assert_response :success, "Wrong response status for #{assoc_sort} sort"
1195 1195
1196 1196 assert_select 'div.next-prev-links' do
1197 1197 assert_select 'a', :text => /(Previous|Next)/
1198 1198 end
1199 1199 end
1200 1200 end
1201 1201
1202 1202 def test_show_should_display_prev_next_links_with_project_query_in_session
1203 1203 @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'o'}}, :project_id => 1}
1204 1204 @request.session['issues_index_sort'] = 'id'
1205 1205
1206 1206 with_settings :display_subprojects_issues => '0' do
1207 1207 get :show, :id => 3
1208 1208 end
1209 1209
1210 1210 assert_response :success
1211 1211 # Previous and next issues inside project
1212 1212 assert_equal 2, assigns(:prev_issue_id)
1213 1213 assert_equal 7, assigns(:next_issue_id)
1214 1214
1215 1215 assert_select 'div.next-prev-links' do
1216 1216 assert_select 'a[href=/issues/2]', :text => /Previous/
1217 1217 assert_select 'a[href=/issues/7]', :text => /Next/
1218 1218 end
1219 1219 end
1220 1220
1221 1221 def test_show_should_not_display_prev_link_for_first_issue
1222 1222 @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'o'}}, :project_id => 1}
1223 1223 @request.session['issues_index_sort'] = 'id'
1224 1224
1225 1225 with_settings :display_subprojects_issues => '0' do
1226 1226 get :show, :id => 1
1227 1227 end
1228 1228
1229 1229 assert_response :success
1230 1230 assert_nil assigns(:prev_issue_id)
1231 1231 assert_equal 2, assigns(:next_issue_id)
1232 1232
1233 1233 assert_select 'div.next-prev-links' do
1234 1234 assert_select 'a', :text => /Previous/, :count => 0
1235 1235 assert_select 'a[href=/issues/2]', :text => /Next/
1236 1236 end
1237 1237 end
1238 1238
1239 1239 def test_show_should_not_display_prev_next_links_for_issue_not_in_query_results
1240 1240 @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'c'}}, :project_id => 1}
1241 1241 @request.session['issues_index_sort'] = 'id'
1242 1242
1243 1243 get :show, :id => 1
1244 1244
1245 1245 assert_response :success
1246 1246 assert_nil assigns(:prev_issue_id)
1247 1247 assert_nil assigns(:next_issue_id)
1248 1248
1249 1249 assert_select 'a', :text => /Previous/, :count => 0
1250 1250 assert_select 'a', :text => /Next/, :count => 0
1251 1251 end
1252 1252
1253 1253 def test_show_show_should_display_prev_next_links_with_query_sort_by_user_custom_field
1254 1254 cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1,2,3], :field_format => 'user')
1255 1255 CustomValue.create!(:custom_field => cf, :customized => Issue.find(1), :value => '2')
1256 1256 CustomValue.create!(:custom_field => cf, :customized => Issue.find(2), :value => '3')
1257 1257 CustomValue.create!(:custom_field => cf, :customized => Issue.find(3), :value => '3')
1258 1258 CustomValue.create!(:custom_field => cf, :customized => Issue.find(5), :value => '')
1259 1259
1260 1260 query = IssueQuery.create!(:name => 'test', :visibility => IssueQuery::VISIBILITY_PUBLIC, :user_id => 1, :filters => {},
1261 1261 :sort_criteria => [["cf_#{cf.id}", 'asc'], ['id', 'asc']])
1262 1262 @request.session[:query] = {:id => query.id, :project_id => nil}
1263 1263
1264 1264 get :show, :id => 3
1265 1265 assert_response :success
1266 1266
1267 1267 assert_equal 2, assigns(:prev_issue_id)
1268 1268 assert_equal 1, assigns(:next_issue_id)
1269 1269
1270 1270 assert_select 'div.next-prev-links' do
1271 1271 assert_select 'a[href=/issues/2]', :text => /Previous/
1272 1272 assert_select 'a[href=/issues/1]', :text => /Next/
1273 1273 end
1274 1274 end
1275 1275
1276 1276 def test_show_should_display_link_to_the_assignee
1277 1277 get :show, :id => 2
1278 1278 assert_response :success
1279 1279 assert_select '.assigned-to' do
1280 1280 assert_select 'a[href=/users/3]'
1281 1281 end
1282 1282 end
1283 1283
1284 1284 def test_show_should_display_visible_changesets_from_other_projects
1285 1285 project = Project.find(2)
1286 1286 issue = project.issues.first
1287 1287 issue.changeset_ids = [102]
1288 1288 issue.save!
1289 1289 # changesets from other projects should be displayed even if repository
1290 1290 # is disabled on issue's project
1291 1291 project.disable_module! :repository
1292 1292
1293 1293 @request.session[:user_id] = 2
1294 1294 get :show, :id => issue.id
1295 1295
1296 1296 assert_select 'a[href=?]', '/projects/ecookbook/repository/revisions/3'
1297 1297 end
1298 1298
1299 1299 def test_show_should_display_watchers
1300 1300 @request.session[:user_id] = 2
1301 1301 Issue.find(1).add_watcher User.find(2)
1302 1302
1303 1303 get :show, :id => 1
1304 1304 assert_select 'div#watchers ul' do
1305 1305 assert_select 'li' do
1306 1306 assert_select 'a[href=/users/2]'
1307 1307 assert_select 'a img[alt=Delete]'
1308 1308 end
1309 1309 end
1310 1310 end
1311 1311
1312 1312 def test_show_should_display_watchers_with_gravatars
1313 1313 @request.session[:user_id] = 2
1314 1314 Issue.find(1).add_watcher User.find(2)
1315 1315
1316 1316 with_settings :gravatar_enabled => '1' do
1317 1317 get :show, :id => 1
1318 1318 end
1319 1319
1320 1320 assert_select 'div#watchers ul' do
1321 1321 assert_select 'li' do
1322 1322 assert_select 'img.gravatar'
1323 1323 assert_select 'a[href=/users/2]'
1324 1324 assert_select 'a img[alt=Delete]'
1325 1325 end
1326 1326 end
1327 1327 end
1328 1328
1329 1329 def test_show_with_thumbnails_enabled_should_display_thumbnails
1330 1330 @request.session[:user_id] = 2
1331 1331
1332 1332 with_settings :thumbnails_enabled => '1' do
1333 1333 get :show, :id => 14
1334 1334 assert_response :success
1335 1335 end
1336 1336
1337 1337 assert_select 'div.thumbnails' do
1338 1338 assert_select 'a[href=/attachments/16/testfile.png]' do
1339 1339 assert_select 'img[src=/attachments/thumbnail/16]'
1340 1340 end
1341 1341 end
1342 1342 end
1343 1343
1344 1344 def test_show_with_thumbnails_disabled_should_not_display_thumbnails
1345 1345 @request.session[:user_id] = 2
1346 1346
1347 1347 with_settings :thumbnails_enabled => '0' do
1348 1348 get :show, :id => 14
1349 1349 assert_response :success
1350 1350 end
1351 1351
1352 1352 assert_select 'div.thumbnails', 0
1353 1353 end
1354 1354
1355 1355 def test_show_with_multi_custom_field
1356 1356 field = CustomField.find(1)
1357 1357 field.update_attribute :multiple, true
1358 1358 issue = Issue.find(1)
1359 1359 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
1360 1360 issue.save!
1361 1361
1362 1362 get :show, :id => 1
1363 1363 assert_response :success
1364 1364
1365 1365 assert_select 'td', :text => 'MySQL, Oracle'
1366 1366 end
1367 1367
1368 1368 def test_show_with_multi_user_custom_field
1369 1369 field = IssueCustomField.create!(:name => 'Multi user', :field_format => 'user', :multiple => true,
1370 1370 :tracker_ids => [1], :is_for_all => true)
1371 1371 issue = Issue.find(1)
1372 1372 issue.custom_field_values = {field.id => ['2', '3']}
1373 1373 issue.save!
1374 1374
1375 1375 get :show, :id => 1
1376 1376 assert_response :success
1377 1377
1378 1378 assert_select "td.cf_#{field.id}", :text => 'Dave Lopper, John Smith' do
1379 1379 assert_select 'a', :text => 'Dave Lopper'
1380 1380 assert_select 'a', :text => 'John Smith'
1381 1381 end
1382 1382 end
1383 1383
1384 1384 def test_show_should_display_private_notes_with_permission_only
1385 1385 journal = Journal.create!(:journalized => Issue.find(2), :notes => 'Privates notes', :private_notes => true, :user_id => 1)
1386 1386 @request.session[:user_id] = 2
1387 1387
1388 1388 get :show, :id => 2
1389 1389 assert_response :success
1390 1390 assert_include journal, assigns(:journals)
1391 1391
1392 1392 Role.find(1).remove_permission! :view_private_notes
1393 1393 get :show, :id => 2
1394 1394 assert_response :success
1395 1395 assert_not_include journal, assigns(:journals)
1396 1396 end
1397 1397
1398 1398 def test_show_atom
1399 1399 get :show, :id => 2, :format => 'atom'
1400 1400 assert_response :success
1401 1401 assert_template 'journals/index'
1402 1402 # Inline image
1403 1403 assert_select 'content', :text => Regexp.new(Regexp.quote('http://test.host/attachments/download/10'))
1404 1404 end
1405 1405
1406 1406 def test_show_export_to_pdf
1407 1407 get :show, :id => 3, :format => 'pdf'
1408 1408 assert_response :success
1409 1409 assert_equal 'application/pdf', @response.content_type
1410 1410 assert @response.body.starts_with?('%PDF')
1411 1411 assert_not_nil assigns(:issue)
1412 1412 end
1413 1413
1414 1414 def test_show_export_to_pdf_with_ancestors
1415 1415 issue = Issue.generate!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'child', :parent_issue_id => 1)
1416 1416
1417 1417 get :show, :id => issue.id, :format => 'pdf'
1418 1418 assert_response :success
1419 1419 assert_equal 'application/pdf', @response.content_type
1420 1420 assert @response.body.starts_with?('%PDF')
1421 1421 end
1422 1422
1423 1423 def test_show_export_to_pdf_with_descendants
1424 1424 c1 = Issue.generate!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'child', :parent_issue_id => 1)
1425 1425 c2 = Issue.generate!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'child', :parent_issue_id => 1)
1426 1426 c3 = Issue.generate!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'child', :parent_issue_id => c1.id)
1427 1427
1428 1428 get :show, :id => 1, :format => 'pdf'
1429 1429 assert_response :success
1430 1430 assert_equal 'application/pdf', @response.content_type
1431 1431 assert @response.body.starts_with?('%PDF')
1432 1432 end
1433 1433
1434 1434 def test_show_export_to_pdf_with_journals
1435 1435 get :show, :id => 1, :format => 'pdf'
1436 1436 assert_response :success
1437 1437 assert_equal 'application/pdf', @response.content_type
1438 1438 assert @response.body.starts_with?('%PDF')
1439 1439 end
1440 1440
1441 1441 def test_show_export_to_pdf_with_changesets
1442 1442 [[100], [100, 101], [100, 101, 102]].each do |cs|
1443 1443 issue1 = Issue.find(3)
1444 1444 issue1.changesets = Changeset.find(cs)
1445 1445 issue1.save!
1446 1446 issue = Issue.find(3)
1447 1447 assert_equal issue.changesets.count, cs.size
1448 1448 get :show, :id => 3, :format => 'pdf'
1449 1449 assert_response :success
1450 1450 assert_equal 'application/pdf', @response.content_type
1451 1451 assert @response.body.starts_with?('%PDF')
1452 1452 end
1453 1453 end
1454 1454
1455 1455 def test_show_invalid_should_respond_with_404
1456 1456 get :show, :id => 999
1457 1457 assert_response 404
1458 1458 end
1459 1459
1460 1460 def test_get_new
1461 1461 @request.session[:user_id] = 2
1462 1462 get :new, :project_id => 1, :tracker_id => 1
1463 1463 assert_response :success
1464 1464 assert_template 'new'
1465 1465
1466 1466 assert_select 'form#issue-form' do
1467 1467 assert_select 'input[name=?]', 'issue[is_private]'
1468 1468 assert_select 'select[name=?]', 'issue[project_id]', 0
1469 1469 assert_select 'select[name=?]', 'issue[tracker_id]'
1470 1470 assert_select 'input[name=?]', 'issue[subject]'
1471 1471 assert_select 'textarea[name=?]', 'issue[description]'
1472 1472 assert_select 'select[name=?]', 'issue[status_id]'
1473 1473 assert_select 'select[name=?]', 'issue[priority_id]'
1474 1474 assert_select 'select[name=?]', 'issue[assigned_to_id]'
1475 1475 assert_select 'select[name=?]', 'issue[category_id]'
1476 1476 assert_select 'select[name=?]', 'issue[fixed_version_id]'
1477 1477 assert_select 'input[name=?]', 'issue[parent_issue_id]'
1478 1478 assert_select 'input[name=?]', 'issue[start_date]'
1479 1479 assert_select 'input[name=?]', 'issue[due_date]'
1480 1480 assert_select 'select[name=?]', 'issue[done_ratio]'
1481 1481 assert_select 'input[name=?][value=?]', 'issue[custom_field_values][2]', 'Default string'
1482 1482 assert_select 'input[name=?]', 'issue[watcher_user_ids][]'
1483 1483 end
1484 1484
1485 1485 # Be sure we don't display inactive IssuePriorities
1486 1486 assert ! IssuePriority.find(15).active?
1487 1487 assert_select 'select[name=?]', 'issue[priority_id]' do
1488 1488 assert_select 'option[value=15]', 0
1489 1489 end
1490 1490 end
1491 1491
1492 1492 def test_get_new_with_minimal_permissions
1493 1493 Role.find(1).update_attribute :permissions, [:add_issues]
1494 1494 WorkflowTransition.delete_all :role_id => 1
1495 1495
1496 1496 @request.session[:user_id] = 2
1497 1497 get :new, :project_id => 1, :tracker_id => 1
1498 1498 assert_response :success
1499 1499 assert_template 'new'
1500 1500
1501 1501 assert_select 'form#issue-form' do
1502 1502 assert_select 'input[name=?]', 'issue[is_private]', 0
1503 1503 assert_select 'select[name=?]', 'issue[project_id]', 0
1504 1504 assert_select 'select[name=?]', 'issue[tracker_id]'
1505 1505 assert_select 'input[name=?]', 'issue[subject]'
1506 1506 assert_select 'textarea[name=?]', 'issue[description]'
1507 1507 assert_select 'select[name=?]', 'issue[status_id]'
1508 1508 assert_select 'select[name=?]', 'issue[priority_id]'
1509 1509 assert_select 'select[name=?]', 'issue[assigned_to_id]'
1510 1510 assert_select 'select[name=?]', 'issue[category_id]'
1511 1511 assert_select 'select[name=?]', 'issue[fixed_version_id]'
1512 1512 assert_select 'input[name=?]', 'issue[parent_issue_id]', 0
1513 1513 assert_select 'input[name=?]', 'issue[start_date]'
1514 1514 assert_select 'input[name=?]', 'issue[due_date]'
1515 1515 assert_select 'select[name=?]', 'issue[done_ratio]'
1516 1516 assert_select 'input[name=?][value=?]', 'issue[custom_field_values][2]', 'Default string'
1517 1517 assert_select 'input[name=?]', 'issue[watcher_user_ids][]', 0
1518 1518 end
1519 1519 end
1520 1520
1521 1521 def test_get_new_with_list_custom_field
1522 1522 @request.session[:user_id] = 2
1523 1523 get :new, :project_id => 1, :tracker_id => 1
1524 1524 assert_response :success
1525 1525 assert_template 'new'
1526 1526
1527 1527 assert_select 'select.list_cf[name=?]', 'issue[custom_field_values][1]' do
1528 1528 assert_select 'option', 4
1529 1529 assert_select 'option[value=MySQL]', :text => 'MySQL'
1530 1530 end
1531 1531 end
1532 1532
1533 1533 def test_get_new_with_multi_custom_field
1534 1534 field = IssueCustomField.find(1)
1535 1535 field.update_attribute :multiple, true
1536 1536
1537 1537 @request.session[:user_id] = 2
1538 1538 get :new, :project_id => 1, :tracker_id => 1
1539 1539 assert_response :success
1540 1540 assert_template 'new'
1541 1541
1542 1542 assert_select 'select[name=?][multiple=multiple]', 'issue[custom_field_values][1][]' do
1543 1543 assert_select 'option', 3
1544 1544 assert_select 'option[value=MySQL]', :text => 'MySQL'
1545 1545 end
1546 1546 assert_select 'input[name=?][type=hidden][value=?]', 'issue[custom_field_values][1][]', ''
1547 1547 end
1548 1548
1549 1549 def test_get_new_with_multi_user_custom_field
1550 1550 field = IssueCustomField.create!(:name => 'Multi user', :field_format => 'user', :multiple => true,
1551 1551 :tracker_ids => [1], :is_for_all => true)
1552 1552
1553 1553 @request.session[:user_id] = 2
1554 1554 get :new, :project_id => 1, :tracker_id => 1
1555 1555 assert_response :success
1556 1556 assert_template 'new'
1557 1557
1558 1558 assert_select 'select[name=?][multiple=multiple]', "issue[custom_field_values][#{field.id}][]" do
1559 1559 assert_select 'option', Project.find(1).users.count
1560 1560 assert_select 'option[value=2]', :text => 'John Smith'
1561 1561 end
1562 1562 assert_select 'input[name=?][type=hidden][value=?]', "issue[custom_field_values][#{field.id}][]", ''
1563 1563 end
1564 1564
1565 1565 def test_get_new_with_date_custom_field
1566 1566 field = IssueCustomField.create!(:name => 'Date', :field_format => 'date', :tracker_ids => [1], :is_for_all => true)
1567 1567
1568 1568 @request.session[:user_id] = 2
1569 1569 get :new, :project_id => 1, :tracker_id => 1
1570 1570 assert_response :success
1571 1571
1572 1572 assert_select 'input[name=?]', "issue[custom_field_values][#{field.id}]"
1573 1573 end
1574 1574
1575 1575 def test_get_new_with_text_custom_field
1576 1576 field = IssueCustomField.create!(:name => 'Text', :field_format => 'text', :tracker_ids => [1], :is_for_all => true)
1577 1577
1578 1578 @request.session[:user_id] = 2
1579 1579 get :new, :project_id => 1, :tracker_id => 1
1580 1580 assert_response :success
1581 1581
1582 1582 assert_select 'textarea[name=?]', "issue[custom_field_values][#{field.id}]"
1583 1583 end
1584 1584
1585 1585 def test_get_new_without_default_start_date_is_creation_date
1586 1586 with_settings :default_issue_start_date_to_creation_date => 0 do
1587 1587 @request.session[:user_id] = 2
1588 1588 get :new, :project_id => 1, :tracker_id => 1
1589 1589 assert_response :success
1590 1590 assert_template 'new'
1591 1591 assert_select 'input[name=?]', 'issue[start_date]'
1592 1592 assert_select 'input[name=?][value]', 'issue[start_date]', 0
1593 1593 end
1594 1594 end
1595 1595
1596 1596 def test_get_new_with_default_start_date_is_creation_date
1597 1597 with_settings :default_issue_start_date_to_creation_date => 1 do
1598 1598 @request.session[:user_id] = 2
1599 1599 get :new, :project_id => 1, :tracker_id => 1
1600 1600 assert_response :success
1601 1601 assert_template 'new'
1602 1602 assert_select 'input[name=?][value=?]', 'issue[start_date]',
1603 1603 Date.today.to_s
1604 1604 end
1605 1605 end
1606 1606
1607 1607 def test_get_new_form_should_allow_attachment_upload
1608 1608 @request.session[:user_id] = 2
1609 1609 get :new, :project_id => 1, :tracker_id => 1
1610 1610
1611 1611 assert_select 'form[id=issue-form][method=post][enctype=multipart/form-data]' do
1612 1612 assert_select 'input[name=?][type=file]', 'attachments[dummy][file]'
1613 1613 end
1614 1614 end
1615 1615
1616 1616 def test_get_new_should_prefill_the_form_from_params
1617 1617 @request.session[:user_id] = 2
1618 1618 get :new, :project_id => 1,
1619 1619 :issue => {:tracker_id => 3, :description => 'Prefilled', :custom_field_values => {'2' => 'Custom field value'}}
1620 1620
1621 1621 issue = assigns(:issue)
1622 1622 assert_equal 3, issue.tracker_id
1623 1623 assert_equal 'Prefilled', issue.description
1624 1624 assert_equal 'Custom field value', issue.custom_field_value(2)
1625 1625
1626 1626 assert_select 'select[name=?]', 'issue[tracker_id]' do
1627 1627 assert_select 'option[value=3][selected=selected]'
1628 1628 end
1629 1629 assert_select 'textarea[name=?]', 'issue[description]', :text => /Prefilled/
1630 1630 assert_select 'input[name=?][value=?]', 'issue[custom_field_values][2]', 'Custom field value'
1631 1631 end
1632 1632
1633 1633 def test_get_new_should_mark_required_fields
1634 1634 cf1 = IssueCustomField.create!(:name => 'Foo', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2])
1635 1635 cf2 = IssueCustomField.create!(:name => 'Bar', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2])
1636 1636 WorkflowPermission.delete_all
1637 1637 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'due_date', :rule => 'required')
1638 1638 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => cf2.id.to_s, :rule => 'required')
1639 1639 @request.session[:user_id] = 2
1640 1640
1641 1641 get :new, :project_id => 1
1642 1642 assert_response :success
1643 1643 assert_template 'new'
1644 1644
1645 1645 assert_select 'label[for=issue_start_date]' do
1646 1646 assert_select 'span[class=required]', 0
1647 1647 end
1648 1648 assert_select 'label[for=issue_due_date]' do
1649 1649 assert_select 'span[class=required]'
1650 1650 end
1651 1651 assert_select 'label[for=?]', "issue_custom_field_values_#{cf1.id}" do
1652 1652 assert_select 'span[class=required]', 0
1653 1653 end
1654 1654 assert_select 'label[for=?]', "issue_custom_field_values_#{cf2.id}" do
1655 1655 assert_select 'span[class=required]'
1656 1656 end
1657 1657 end
1658 1658
1659 1659 def test_get_new_should_not_display_readonly_fields
1660 1660 cf1 = IssueCustomField.create!(:name => 'Foo', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2])
1661 1661 cf2 = IssueCustomField.create!(:name => 'Bar', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2])
1662 1662 WorkflowPermission.delete_all
1663 1663 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'due_date', :rule => 'readonly')
1664 1664 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => cf2.id.to_s, :rule => 'readonly')
1665 1665 @request.session[:user_id] = 2
1666 1666
1667 1667 get :new, :project_id => 1
1668 1668 assert_response :success
1669 1669 assert_template 'new'
1670 1670
1671 1671 assert_select 'input[name=?]', 'issue[start_date]'
1672 1672 assert_select 'input[name=?]', 'issue[due_date]', 0
1673 1673 assert_select 'input[name=?]', "issue[custom_field_values][#{cf1.id}]"
1674 1674 assert_select 'input[name=?]', "issue[custom_field_values][#{cf2.id}]", 0
1675 1675 end
1676 1676
1677 1677 def test_get_new_without_tracker_id
1678 1678 @request.session[:user_id] = 2
1679 1679 get :new, :project_id => 1
1680 1680 assert_response :success
1681 1681 assert_template 'new'
1682 1682
1683 1683 issue = assigns(:issue)
1684 1684 assert_not_nil issue
1685 1685 assert_equal Project.find(1).trackers.first, issue.tracker
1686 1686 end
1687 1687
1688 1688 def test_get_new_with_no_default_status_should_display_an_error
1689 1689 @request.session[:user_id] = 2
1690 1690 IssueStatus.delete_all
1691 1691
1692 1692 get :new, :project_id => 1
1693 1693 assert_response 500
1694 1694 assert_error_tag :content => /No default issue/
1695 1695 end
1696 1696
1697 1697 def test_get_new_with_no_tracker_should_display_an_error
1698 1698 @request.session[:user_id] = 2
1699 1699 Tracker.delete_all
1700 1700
1701 1701 get :new, :project_id => 1
1702 1702 assert_response 500
1703 1703 assert_error_tag :content => /No tracker/
1704 1704 end
1705 1705
1706 1706 def test_update_form_for_new_issue
1707 1707 @request.session[:user_id] = 2
1708 1708 xhr :post, :update_form, :project_id => 1,
1709 1709 :issue => {:tracker_id => 2,
1710 1710 :subject => 'This is the test_new issue',
1711 1711 :description => 'This is the description',
1712 1712 :priority_id => 5}
1713 1713 assert_response :success
1714 1714 assert_template 'update_form'
1715 1715 assert_template :partial => '_form'
1716 1716 assert_equal 'text/javascript', response.content_type
1717 1717
1718 1718 issue = assigns(:issue)
1719 1719 assert_kind_of Issue, issue
1720 1720 assert_equal 1, issue.project_id
1721 1721 assert_equal 2, issue.tracker_id
1722 1722 assert_equal 'This is the test_new issue', issue.subject
1723 1723 end
1724 1724
1725 1725 def test_update_form_for_new_issue_should_propose_transitions_based_on_initial_status
1726 1726 @request.session[:user_id] = 2
1727 1727 WorkflowTransition.delete_all
1728 1728 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 2)
1729 1729 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 5)
1730 1730 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 5, :new_status_id => 4)
1731 1731
1732 1732 xhr :post, :update_form, :project_id => 1,
1733 1733 :issue => {:tracker_id => 1,
1734 1734 :status_id => 5,
1735 1735 :subject => 'This is an issue'}
1736 1736
1737 1737 assert_equal 5, assigns(:issue).status_id
1738 1738 assert_equal [1,2,5], assigns(:allowed_statuses).map(&:id).sort
1739 1739 end
1740 1740
1741 1741 def test_post_create
1742 1742 @request.session[:user_id] = 2
1743 1743 assert_difference 'Issue.count' do
1744 1744 post :create, :project_id => 1,
1745 1745 :issue => {:tracker_id => 3,
1746 1746 :status_id => 2,
1747 1747 :subject => 'This is the test_new issue',
1748 1748 :description => 'This is the description',
1749 1749 :priority_id => 5,
1750 1750 :start_date => '2010-11-07',
1751 1751 :estimated_hours => '',
1752 1752 :custom_field_values => {'2' => 'Value for field 2'}}
1753 1753 end
1754 1754 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
1755 1755
1756 1756 issue = Issue.find_by_subject('This is the test_new issue')
1757 1757 assert_not_nil issue
1758 1758 assert_equal 2, issue.author_id
1759 1759 assert_equal 3, issue.tracker_id
1760 1760 assert_equal 2, issue.status_id
1761 1761 assert_equal Date.parse('2010-11-07'), issue.start_date
1762 1762 assert_nil issue.estimated_hours
1763 1763 v = issue.custom_values.where(:custom_field_id => 2).first
1764 1764 assert_not_nil v
1765 1765 assert_equal 'Value for field 2', v.value
1766 1766 end
1767 1767
1768 1768 def test_post_new_with_group_assignment
1769 1769 group = Group.find(11)
1770 1770 project = Project.find(1)
1771 1771 project.members << Member.new(:principal => group, :roles => [Role.givable.first])
1772 1772
1773 1773 with_settings :issue_group_assignment => '1' do
1774 1774 @request.session[:user_id] = 2
1775 1775 assert_difference 'Issue.count' do
1776 1776 post :create, :project_id => project.id,
1777 1777 :issue => {:tracker_id => 3,
1778 1778 :status_id => 1,
1779 1779 :subject => 'This is the test_new_with_group_assignment issue',
1780 1780 :assigned_to_id => group.id}
1781 1781 end
1782 1782 end
1783 1783 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
1784 1784
1785 1785 issue = Issue.find_by_subject('This is the test_new_with_group_assignment issue')
1786 1786 assert_not_nil issue
1787 1787 assert_equal group, issue.assigned_to
1788 1788 end
1789 1789
1790 1790 def test_post_create_without_start_date_and_default_start_date_is_not_creation_date
1791 1791 with_settings :default_issue_start_date_to_creation_date => 0 do
1792 1792 @request.session[:user_id] = 2
1793 1793 assert_difference 'Issue.count' do
1794 1794 post :create, :project_id => 1,
1795 1795 :issue => {:tracker_id => 3,
1796 1796 :status_id => 2,
1797 1797 :subject => 'This is the test_new issue',
1798 1798 :description => 'This is the description',
1799 1799 :priority_id => 5,
1800 1800 :estimated_hours => '',
1801 1801 :custom_field_values => {'2' => 'Value for field 2'}}
1802 1802 end
1803 1803 assert_redirected_to :controller => 'issues', :action => 'show',
1804 1804 :id => Issue.last.id
1805 1805 issue = Issue.find_by_subject('This is the test_new issue')
1806 1806 assert_not_nil issue
1807 1807 assert_nil issue.start_date
1808 1808 end
1809 1809 end
1810 1810
1811 1811 def test_post_create_without_start_date_and_default_start_date_is_creation_date
1812 1812 with_settings :default_issue_start_date_to_creation_date => 1 do
1813 1813 @request.session[:user_id] = 2
1814 1814 assert_difference 'Issue.count' do
1815 1815 post :create, :project_id => 1,
1816 1816 :issue => {:tracker_id => 3,
1817 1817 :status_id => 2,
1818 1818 :subject => 'This is the test_new issue',
1819 1819 :description => 'This is the description',
1820 1820 :priority_id => 5,
1821 1821 :estimated_hours => '',
1822 1822 :custom_field_values => {'2' => 'Value for field 2'}}
1823 1823 end
1824 1824 assert_redirected_to :controller => 'issues', :action => 'show',
1825 1825 :id => Issue.last.id
1826 1826 issue = Issue.find_by_subject('This is the test_new issue')
1827 1827 assert_not_nil issue
1828 1828 assert_equal Date.today, issue.start_date
1829 1829 end
1830 1830 end
1831 1831
1832 1832 def test_post_create_and_continue
1833 1833 @request.session[:user_id] = 2
1834 1834 assert_difference 'Issue.count' do
1835 1835 post :create, :project_id => 1,
1836 1836 :issue => {:tracker_id => 3, :subject => 'This is first issue', :priority_id => 5},
1837 1837 :continue => ''
1838 1838 end
1839 1839
1840 1840 issue = Issue.order('id DESC').first
1841 1841 assert_redirected_to :controller => 'issues', :action => 'new', :project_id => 'ecookbook', :issue => {:tracker_id => 3}
1842 1842 assert_not_nil flash[:notice], "flash was not set"
1843 1843 assert_include %|<a href="/issues/#{issue.id}" title="This is first issue">##{issue.id}</a>|, flash[:notice], "issue link not found in the flash message"
1844 1844 end
1845 1845
1846 1846 def test_post_create_without_custom_fields_param
1847 1847 @request.session[:user_id] = 2
1848 1848 assert_difference 'Issue.count' do
1849 1849 post :create, :project_id => 1,
1850 1850 :issue => {:tracker_id => 1,
1851 1851 :subject => 'This is the test_new issue',
1852 1852 :description => 'This is the description',
1853 1853 :priority_id => 5}
1854 1854 end
1855 1855 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
1856 1856 end
1857 1857
1858 1858 def test_post_create_with_multi_custom_field
1859 1859 field = IssueCustomField.find_by_name('Database')
1860 1860 field.update_attribute(:multiple, true)
1861 1861
1862 1862 @request.session[:user_id] = 2
1863 1863 assert_difference 'Issue.count' do
1864 1864 post :create, :project_id => 1,
1865 1865 :issue => {:tracker_id => 1,
1866 1866 :subject => 'This is the test_new issue',
1867 1867 :description => 'This is the description',
1868 1868 :priority_id => 5,
1869 1869 :custom_field_values => {'1' => ['', 'MySQL', 'Oracle']}}
1870 1870 end
1871 1871 assert_response 302
1872 1872 issue = Issue.order('id DESC').first
1873 1873 assert_equal ['MySQL', 'Oracle'], issue.custom_field_value(1).sort
1874 1874 end
1875 1875
1876 1876 def test_post_create_with_empty_multi_custom_field
1877 1877 field = IssueCustomField.find_by_name('Database')
1878 1878 field.update_attribute(:multiple, true)
1879 1879
1880 1880 @request.session[:user_id] = 2
1881 1881 assert_difference 'Issue.count' do
1882 1882 post :create, :project_id => 1,
1883 1883 :issue => {:tracker_id => 1,
1884 1884 :subject => 'This is the test_new issue',
1885 1885 :description => 'This is the description',
1886 1886 :priority_id => 5,
1887 1887 :custom_field_values => {'1' => ['']}}
1888 1888 end
1889 1889 assert_response 302
1890 1890 issue = Issue.order('id DESC').first
1891 1891 assert_equal [''], issue.custom_field_value(1).sort
1892 1892 end
1893 1893
1894 1894 def test_post_create_with_multi_user_custom_field
1895 1895 field = IssueCustomField.create!(:name => 'Multi user', :field_format => 'user', :multiple => true,
1896 1896 :tracker_ids => [1], :is_for_all => true)
1897 1897
1898 1898 @request.session[:user_id] = 2
1899 1899 assert_difference 'Issue.count' do
1900 1900 post :create, :project_id => 1,
1901 1901 :issue => {:tracker_id => 1,
1902 1902 :subject => 'This is the test_new issue',
1903 1903 :description => 'This is the description',
1904 1904 :priority_id => 5,
1905 1905 :custom_field_values => {field.id.to_s => ['', '2', '3']}}
1906 1906 end
1907 1907 assert_response 302
1908 1908 issue = Issue.order('id DESC').first
1909 1909 assert_equal ['2', '3'], issue.custom_field_value(field).sort
1910 1910 end
1911 1911
1912 1912 def test_post_create_with_required_custom_field_and_without_custom_fields_param
1913 1913 field = IssueCustomField.find_by_name('Database')
1914 1914 field.update_attribute(:is_required, true)
1915 1915
1916 1916 @request.session[:user_id] = 2
1917 1917 assert_no_difference 'Issue.count' do
1918 1918 post :create, :project_id => 1,
1919 1919 :issue => {:tracker_id => 1,
1920 1920 :subject => 'This is the test_new issue',
1921 1921 :description => 'This is the description',
1922 1922 :priority_id => 5}
1923 1923 end
1924 1924 assert_response :success
1925 1925 assert_template 'new'
1926 1926 issue = assigns(:issue)
1927 1927 assert_not_nil issue
1928 1928 assert_error_tag :content => /Database #{ESCAPED_CANT} be blank/
1929 1929 end
1930 1930
1931 1931 def test_create_should_validate_required_fields
1932 1932 cf1 = IssueCustomField.create!(:name => 'Foo', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2])
1933 1933 cf2 = IssueCustomField.create!(:name => 'Bar', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2])
1934 1934 WorkflowPermission.delete_all
1935 1935 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, :role_id => 1, :field_name => 'due_date', :rule => 'required')
1936 1936 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, :role_id => 1, :field_name => cf2.id.to_s, :rule => 'required')
1937 1937 @request.session[:user_id] = 2
1938 1938
1939 1939 assert_no_difference 'Issue.count' do
1940 1940 post :create, :project_id => 1, :issue => {
1941 1941 :tracker_id => 2,
1942 1942 :status_id => 1,
1943 1943 :subject => 'Test',
1944 1944 :start_date => '',
1945 1945 :due_date => '',
1946 1946 :custom_field_values => {cf1.id.to_s => '', cf2.id.to_s => ''}
1947 1947 }
1948 1948 assert_response :success
1949 1949 assert_template 'new'
1950 1950 end
1951 1951
1952 1952 assert_error_tag :content => /Due date #{ESCAPED_CANT} be blank/i
1953 1953 assert_error_tag :content => /Bar #{ESCAPED_CANT} be blank/i
1954 1954 end
1955 1955
1956 1956 def test_create_should_ignore_readonly_fields
1957 1957 cf1 = IssueCustomField.create!(:name => 'Foo', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2])
1958 1958 cf2 = IssueCustomField.create!(:name => 'Bar', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2])
1959 1959 WorkflowPermission.delete_all
1960 1960 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, :role_id => 1, :field_name => 'due_date', :rule => 'readonly')
1961 1961 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, :role_id => 1, :field_name => cf2.id.to_s, :rule => 'readonly')
1962 1962 @request.session[:user_id] = 2
1963 1963
1964 1964 assert_difference 'Issue.count' do
1965 1965 post :create, :project_id => 1, :issue => {
1966 1966 :tracker_id => 2,
1967 1967 :status_id => 1,
1968 1968 :subject => 'Test',
1969 1969 :start_date => '2012-07-14',
1970 1970 :due_date => '2012-07-16',
1971 1971 :custom_field_values => {cf1.id.to_s => 'value1', cf2.id.to_s => 'value2'}
1972 1972 }
1973 1973 assert_response 302
1974 1974 end
1975 1975
1976 1976 issue = Issue.order('id DESC').first
1977 1977 assert_equal Date.parse('2012-07-14'), issue.start_date
1978 1978 assert_nil issue.due_date
1979 1979 assert_equal 'value1', issue.custom_field_value(cf1)
1980 1980 assert_nil issue.custom_field_value(cf2)
1981 1981 end
1982 1982
1983 1983 def test_post_create_with_watchers
1984 1984 @request.session[:user_id] = 2
1985 1985 ActionMailer::Base.deliveries.clear
1986 1986
1987 1987 assert_difference 'Watcher.count', 2 do
1988 1988 post :create, :project_id => 1,
1989 1989 :issue => {:tracker_id => 1,
1990 1990 :subject => 'This is a new issue with watchers',
1991 1991 :description => 'This is the description',
1992 1992 :priority_id => 5,
1993 1993 :watcher_user_ids => ['2', '3']}
1994 1994 end
1995 1995 issue = Issue.find_by_subject('This is a new issue with watchers')
1996 1996 assert_not_nil issue
1997 1997 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
1998 1998
1999 1999 # Watchers added
2000 2000 assert_equal [2, 3], issue.watcher_user_ids.sort
2001 2001 assert issue.watched_by?(User.find(3))
2002 2002 # Watchers notified
2003 2003 mail = ActionMailer::Base.deliveries.last
2004 2004 assert_not_nil mail
2005 2005 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
2006 2006 end
2007 2007
2008 2008 def test_post_create_subissue
2009 2009 @request.session[:user_id] = 2
2010 2010
2011 2011 assert_difference 'Issue.count' do
2012 2012 post :create, :project_id => 1,
2013 2013 :issue => {:tracker_id => 1,
2014 2014 :subject => 'This is a child issue',
2015 2015 :parent_issue_id => '2'}
2016 2016 assert_response 302
2017 2017 end
2018 2018 issue = Issue.order('id DESC').first
2019 2019 assert_equal Issue.find(2), issue.parent
2020 2020 end
2021 2021
2022 2022 def test_post_create_subissue_with_sharp_parent_id
2023 2023 @request.session[:user_id] = 2
2024 2024
2025 2025 assert_difference 'Issue.count' do
2026 2026 post :create, :project_id => 1,
2027 2027 :issue => {:tracker_id => 1,
2028 2028 :subject => 'This is a child issue',
2029 2029 :parent_issue_id => '#2'}
2030 2030 assert_response 302
2031 2031 end
2032 2032 issue = Issue.order('id DESC').first
2033 2033 assert_equal Issue.find(2), issue.parent
2034 2034 end
2035 2035
2036 2036 def test_post_create_subissue_with_non_visible_parent_id_should_not_validate
2037 2037 @request.session[:user_id] = 2
2038 2038
2039 2039 assert_no_difference 'Issue.count' do
2040 2040 post :create, :project_id => 1,
2041 2041 :issue => {:tracker_id => 1,
2042 2042 :subject => 'This is a child issue',
2043 2043 :parent_issue_id => '4'}
2044 2044
2045 2045 assert_response :success
2046 2046 assert_select 'input[name=?][value=?]', 'issue[parent_issue_id]', '4'
2047 2047 assert_error_tag :content => /Parent task is invalid/i
2048 2048 end
2049 2049 end
2050 2050
2051 2051 def test_post_create_subissue_with_non_numeric_parent_id_should_not_validate
2052 2052 @request.session[:user_id] = 2
2053 2053
2054 2054 assert_no_difference 'Issue.count' do
2055 2055 post :create, :project_id => 1,
2056 2056 :issue => {:tracker_id => 1,
2057 2057 :subject => 'This is a child issue',
2058 2058 :parent_issue_id => '01ABC'}
2059 2059
2060 2060 assert_response :success
2061 2061 assert_select 'input[name=?][value=?]', 'issue[parent_issue_id]', '01ABC'
2062 2062 assert_error_tag :content => /Parent task is invalid/i
2063 2063 end
2064 2064 end
2065 2065
2066 2066 def test_post_create_private
2067 2067 @request.session[:user_id] = 2
2068 2068
2069 2069 assert_difference 'Issue.count' do
2070 2070 post :create, :project_id => 1,
2071 2071 :issue => {:tracker_id => 1,
2072 2072 :subject => 'This is a private issue',
2073 2073 :is_private => '1'}
2074 2074 end
2075 2075 issue = Issue.order('id DESC').first
2076 2076 assert issue.is_private?
2077 2077 end
2078 2078
2079 2079 def test_post_create_private_with_set_own_issues_private_permission
2080 2080 role = Role.find(1)
2081 2081 role.remove_permission! :set_issues_private
2082 2082 role.add_permission! :set_own_issues_private
2083 2083
2084 2084 @request.session[:user_id] = 2
2085 2085
2086 2086 assert_difference 'Issue.count' do
2087 2087 post :create, :project_id => 1,
2088 2088 :issue => {:tracker_id => 1,
2089 2089 :subject => 'This is a private issue',
2090 2090 :is_private => '1'}
2091 2091 end
2092 2092 issue = Issue.order('id DESC').first
2093 2093 assert issue.is_private?
2094 2094 end
2095 2095
2096 2096 def test_post_create_should_send_a_notification
2097 2097 ActionMailer::Base.deliveries.clear
2098 2098 @request.session[:user_id] = 2
2099 2099 assert_difference 'Issue.count' do
2100 2100 post :create, :project_id => 1,
2101 2101 :issue => {:tracker_id => 3,
2102 2102 :subject => 'This is the test_new issue',
2103 2103 :description => 'This is the description',
2104 2104 :priority_id => 5,
2105 2105 :estimated_hours => '',
2106 2106 :custom_field_values => {'2' => 'Value for field 2'}}
2107 2107 end
2108 2108 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
2109 2109
2110 2110 assert_equal 1, ActionMailer::Base.deliveries.size
2111 2111 end
2112 2112
2113 2113 def test_post_create_should_preserve_fields_values_on_validation_failure
2114 2114 @request.session[:user_id] = 2
2115 2115 post :create, :project_id => 1,
2116 2116 :issue => {:tracker_id => 1,
2117 2117 # empty subject
2118 2118 :subject => '',
2119 2119 :description => 'This is a description',
2120 2120 :priority_id => 6,
2121 2121 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
2122 2122 assert_response :success
2123 2123 assert_template 'new'
2124 2124
2125 2125 assert_select 'textarea[name=?]', 'issue[description]', :text => 'This is a description'
2126 2126 assert_select 'select[name=?]', 'issue[priority_id]' do
2127 2127 assert_select 'option[value=6][selected=selected]', :text => 'High'
2128 2128 end
2129 2129 # Custom fields
2130 2130 assert_select 'select[name=?]', 'issue[custom_field_values][1]' do
2131 2131 assert_select 'option[value=Oracle][selected=selected]', :text => 'Oracle'
2132 2132 end
2133 2133 assert_select 'input[name=?][value=?]', 'issue[custom_field_values][2]', 'Value for field 2'
2134 2134 end
2135 2135
2136 2136 def test_post_create_with_failure_should_preserve_watchers
2137 2137 assert !User.find(8).member_of?(Project.find(1))
2138 2138
2139 2139 @request.session[:user_id] = 2
2140 2140 post :create, :project_id => 1,
2141 2141 :issue => {:tracker_id => 1,
2142 2142 :watcher_user_ids => ['3', '8']}
2143 2143 assert_response :success
2144 2144 assert_template 'new'
2145 2145
2146 2146 assert_select 'input[name=?][value=2]:not(checked)', 'issue[watcher_user_ids][]'
2147 2147 assert_select 'input[name=?][value=3][checked=checked]', 'issue[watcher_user_ids][]'
2148 2148 assert_select 'input[name=?][value=8][checked=checked]', 'issue[watcher_user_ids][]'
2149 2149 end
2150 2150
2151 2151 def test_post_create_should_ignore_non_safe_attributes
2152 2152 @request.session[:user_id] = 2
2153 2153 assert_nothing_raised do
2154 2154 post :create, :project_id => 1, :issue => { :tracker => "A param can not be a Tracker" }
2155 2155 end
2156 2156 end
2157 2157
2158 2158 def test_post_create_with_attachment
2159 2159 set_tmp_attachments_directory
2160 2160 @request.session[:user_id] = 2
2161 2161
2162 2162 assert_difference 'Issue.count' do
2163 2163 assert_difference 'Attachment.count' do
2164 2164 post :create, :project_id => 1,
2165 2165 :issue => { :tracker_id => '1', :subject => 'With attachment' },
2166 2166 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
2167 2167 end
2168 2168 end
2169 2169
2170 2170 issue = Issue.order('id DESC').first
2171 2171 attachment = Attachment.order('id DESC').first
2172 2172
2173 2173 assert_equal issue, attachment.container
2174 2174 assert_equal 2, attachment.author_id
2175 2175 assert_equal 'testfile.txt', attachment.filename
2176 2176 assert_equal 'text/plain', attachment.content_type
2177 2177 assert_equal 'test file', attachment.description
2178 2178 assert_equal 59, attachment.filesize
2179 2179 assert File.exists?(attachment.diskfile)
2180 2180 assert_equal 59, File.size(attachment.diskfile)
2181 2181 end
2182 2182
2183 2183 def test_post_create_with_attachment_should_notify_with_attachments
2184 2184 ActionMailer::Base.deliveries.clear
2185 2185 set_tmp_attachments_directory
2186 2186 @request.session[:user_id] = 2
2187 2187
2188 2188 with_settings :host_name => 'mydomain.foo', :protocol => 'http' do
2189 2189 assert_difference 'Issue.count' do
2190 2190 post :create, :project_id => 1,
2191 2191 :issue => { :tracker_id => '1', :subject => 'With attachment' },
2192 2192 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
2193 2193 end
2194 2194 end
2195 2195
2196 2196 assert_not_nil ActionMailer::Base.deliveries.last
2197 2197 assert_select_email do
2198 2198 assert_select 'a[href^=?]', 'http://mydomain.foo/attachments/download', 'testfile.txt'
2199 2199 end
2200 2200 end
2201 2201
2202 2202 def test_post_create_with_failure_should_save_attachments
2203 2203 set_tmp_attachments_directory
2204 2204 @request.session[:user_id] = 2
2205 2205
2206 2206 assert_no_difference 'Issue.count' do
2207 2207 assert_difference 'Attachment.count' do
2208 2208 post :create, :project_id => 1,
2209 2209 :issue => { :tracker_id => '1', :subject => '' },
2210 2210 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
2211 2211 assert_response :success
2212 2212 assert_template 'new'
2213 2213 end
2214 2214 end
2215 2215
2216 2216 attachment = Attachment.order('id DESC').first
2217 2217 assert_equal 'testfile.txt', attachment.filename
2218 2218 assert File.exists?(attachment.diskfile)
2219 2219 assert_nil attachment.container
2220 2220
2221 2221 assert_select 'input[name=?][value=?]', 'attachments[p0][token]', attachment.token
2222 2222 assert_select 'input[name=?][value=?]', 'attachments[p0][filename]', 'testfile.txt'
2223 2223 end
2224 2224
2225 2225 def test_post_create_with_failure_should_keep_saved_attachments
2226 2226 set_tmp_attachments_directory
2227 2227 attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2)
2228 2228 @request.session[:user_id] = 2
2229 2229
2230 2230 assert_no_difference 'Issue.count' do
2231 2231 assert_no_difference 'Attachment.count' do
2232 2232 post :create, :project_id => 1,
2233 2233 :issue => { :tracker_id => '1', :subject => '' },
2234 2234 :attachments => {'p0' => {'token' => attachment.token}}
2235 2235 assert_response :success
2236 2236 assert_template 'new'
2237 2237 end
2238 2238 end
2239 2239
2240 2240 assert_select 'input[name=?][value=?]', 'attachments[p0][token]', attachment.token
2241 2241 assert_select 'input[name=?][value=?]', 'attachments[p0][filename]', 'testfile.txt'
2242 2242 end
2243 2243
2244 2244 def test_post_create_should_attach_saved_attachments
2245 2245 set_tmp_attachments_directory
2246 2246 attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2)
2247 2247 @request.session[:user_id] = 2
2248 2248
2249 2249 assert_difference 'Issue.count' do
2250 2250 assert_no_difference 'Attachment.count' do
2251 2251 post :create, :project_id => 1,
2252 2252 :issue => { :tracker_id => '1', :subject => 'Saved attachments' },
2253 2253 :attachments => {'p0' => {'token' => attachment.token}}
2254 2254 assert_response 302
2255 2255 end
2256 2256 end
2257 2257
2258 2258 issue = Issue.order('id DESC').first
2259 2259 assert_equal 1, issue.attachments.count
2260 2260
2261 2261 attachment.reload
2262 2262 assert_equal issue, attachment.container
2263 2263 end
2264 2264
2265 2265 def setup_without_workflow_privilege
2266 2266 WorkflowTransition.delete_all(["role_id = ?", Role.anonymous.id])
2267 2267 Role.anonymous.add_permission! :add_issues, :add_issue_notes
2268 2268 end
2269 2269 private :setup_without_workflow_privilege
2270 2270
2271 2271 test "without workflow privilege #new should propose default status only" do
2272 2272 setup_without_workflow_privilege
2273 2273 get :new, :project_id => 1
2274 2274 assert_response :success
2275 2275 assert_template 'new'
2276 2276 assert_select 'select[name=?]', 'issue[status_id]' do
2277 2277 assert_select 'option', 1
2278 2278 assert_select 'option[value=?]', IssueStatus.default.id.to_s
2279 2279 end
2280 2280 end
2281 2281
2282 2282 test "without workflow privilege #new should accept default status" do
2283 2283 setup_without_workflow_privilege
2284 2284 assert_difference 'Issue.count' do
2285 2285 post :create, :project_id => 1,
2286 2286 :issue => {:tracker_id => 1,
2287 2287 :subject => 'This is an issue',
2288 2288 :status_id => 1}
2289 2289 end
2290 2290 issue = Issue.order('id').last
2291 2291 assert_equal IssueStatus.default, issue.status
2292 2292 end
2293 2293
2294 2294 test "without workflow privilege #new should ignore unauthorized status" do
2295 2295 setup_without_workflow_privilege
2296 2296 assert_difference 'Issue.count' do
2297 2297 post :create, :project_id => 1,
2298 2298 :issue => {:tracker_id => 1,
2299 2299 :subject => 'This is an issue',
2300 2300 :status_id => 3}
2301 2301 end
2302 2302 issue = Issue.order('id').last
2303 2303 assert_equal IssueStatus.default, issue.status
2304 2304 end
2305 2305
2306 2306 test "without workflow privilege #update should ignore status change" do
2307 2307 setup_without_workflow_privilege
2308 2308 assert_difference 'Journal.count' do
2309 2309 put :update, :id => 1, :issue => {:status_id => 3, :notes => 'just trying'}
2310 2310 end
2311 2311 assert_equal 1, Issue.find(1).status_id
2312 2312 end
2313 2313
2314 2314 test "without workflow privilege #update ignore attributes changes" do
2315 2315 setup_without_workflow_privilege
2316 2316 assert_difference 'Journal.count' do
2317 2317 put :update, :id => 1,
2318 2318 :issue => {:subject => 'changed', :assigned_to_id => 2,
2319 2319 :notes => 'just trying'}
2320 2320 end
2321 2321 issue = Issue.find(1)
2322 2322 assert_equal "Can't print recipes", issue.subject
2323 2323 assert_nil issue.assigned_to
2324 2324 end
2325 2325
2326 2326 def setup_with_workflow_privilege
2327 2327 WorkflowTransition.delete_all(["role_id = ?", Role.anonymous.id])
2328 2328 WorkflowTransition.create!(:role => Role.anonymous, :tracker_id => 1,
2329 2329 :old_status_id => 1, :new_status_id => 3)
2330 2330 WorkflowTransition.create!(:role => Role.anonymous, :tracker_id => 1,
2331 2331 :old_status_id => 1, :new_status_id => 4)
2332 2332 Role.anonymous.add_permission! :add_issues, :add_issue_notes
2333 2333 end
2334 2334 private :setup_with_workflow_privilege
2335 2335
2336 2336 test "with workflow privilege #update should accept authorized status" do
2337 2337 setup_with_workflow_privilege
2338 2338 assert_difference 'Journal.count' do
2339 2339 put :update, :id => 1, :issue => {:status_id => 3, :notes => 'just trying'}
2340 2340 end
2341 2341 assert_equal 3, Issue.find(1).status_id
2342 2342 end
2343 2343
2344 2344 test "with workflow privilege #update should ignore unauthorized status" do
2345 2345 setup_with_workflow_privilege
2346 2346 assert_difference 'Journal.count' do
2347 2347 put :update, :id => 1, :issue => {:status_id => 2, :notes => 'just trying'}
2348 2348 end
2349 2349 assert_equal 1, Issue.find(1).status_id
2350 2350 end
2351 2351
2352 2352 test "with workflow privilege #update should accept authorized attributes changes" do
2353 2353 setup_with_workflow_privilege
2354 2354 assert_difference 'Journal.count' do
2355 2355 put :update, :id => 1, :issue => {:assigned_to_id => 2, :notes => 'just trying'}
2356 2356 end
2357 2357 issue = Issue.find(1)
2358 2358 assert_equal 2, issue.assigned_to_id
2359 2359 end
2360 2360
2361 2361 test "with workflow privilege #update should ignore unauthorized attributes changes" do
2362 2362 setup_with_workflow_privilege
2363 2363 assert_difference 'Journal.count' do
2364 2364 put :update, :id => 1, :issue => {:subject => 'changed', :notes => 'just trying'}
2365 2365 end
2366 2366 issue = Issue.find(1)
2367 2367 assert_equal "Can't print recipes", issue.subject
2368 2368 end
2369 2369
2370 2370 def setup_with_workflow_privilege_and_edit_issues_permission
2371 2371 setup_with_workflow_privilege
2372 2372 Role.anonymous.add_permission! :add_issues, :edit_issues
2373 2373 end
2374 2374 private :setup_with_workflow_privilege_and_edit_issues_permission
2375 2375
2376 2376 test "with workflow privilege and :edit_issues permission should accept authorized status" do
2377 2377 setup_with_workflow_privilege_and_edit_issues_permission
2378 2378 assert_difference 'Journal.count' do
2379 2379 put :update, :id => 1, :issue => {:status_id => 3, :notes => 'just trying'}
2380 2380 end
2381 2381 assert_equal 3, Issue.find(1).status_id
2382 2382 end
2383 2383
2384 2384 test "with workflow privilege and :edit_issues permission should ignore unauthorized status" do
2385 2385 setup_with_workflow_privilege_and_edit_issues_permission
2386 2386 assert_difference 'Journal.count' do
2387 2387 put :update, :id => 1, :issue => {:status_id => 2, :notes => 'just trying'}
2388 2388 end
2389 2389 assert_equal 1, Issue.find(1).status_id
2390 2390 end
2391 2391
2392 2392 test "with workflow privilege and :edit_issues permission should accept authorized attributes changes" do
2393 2393 setup_with_workflow_privilege_and_edit_issues_permission
2394 2394 assert_difference 'Journal.count' do
2395 2395 put :update, :id => 1,
2396 2396 :issue => {:subject => 'changed', :assigned_to_id => 2,
2397 2397 :notes => 'just trying'}
2398 2398 end
2399 2399 issue = Issue.find(1)
2400 2400 assert_equal "changed", issue.subject
2401 2401 assert_equal 2, issue.assigned_to_id
2402 2402 end
2403 2403
2404 2404 def test_new_as_copy
2405 2405 @request.session[:user_id] = 2
2406 2406 get :new, :project_id => 1, :copy_from => 1
2407 2407
2408 2408 assert_response :success
2409 2409 assert_template 'new'
2410 2410
2411 2411 assert_not_nil assigns(:issue)
2412 2412 orig = Issue.find(1)
2413 2413 assert_equal 1, assigns(:issue).project_id
2414 2414 assert_equal orig.subject, assigns(:issue).subject
2415 2415 assert assigns(:issue).copy?
2416 2416
2417 2417 assert_select 'form[id=issue-form][action=/projects/ecookbook/issues]' do
2418 2418 assert_select 'select[name=?]', 'issue[project_id]' do
2419 2419 assert_select 'option[value=1][selected=selected]', :text => 'eCookbook'
2420 2420 assert_select 'option[value=2]:not([selected])', :text => 'OnlineStore'
2421 2421 end
2422 2422 assert_select 'input[name=copy_from][value=1]'
2423 2423 end
2424 2424
2425 2425 # "New issue" menu item should not link to copy
2426 2426 assert_select '#main-menu a.new-issue[href=/projects/ecookbook/issues/new]'
2427 2427 end
2428 2428
2429 2429 def test_new_as_copy_with_attachments_should_show_copy_attachments_checkbox
2430 2430 @request.session[:user_id] = 2
2431 2431 issue = Issue.find(3)
2432 2432 assert issue.attachments.count > 0
2433 2433 get :new, :project_id => 1, :copy_from => 3
2434 2434
2435 2435 assert_select 'input[name=copy_attachments][type=checkbox][checked=checked][value=1]'
2436 2436 end
2437 2437
2438 2438 def test_new_as_copy_without_attachments_should_not_show_copy_attachments_checkbox
2439 2439 @request.session[:user_id] = 2
2440 2440 issue = Issue.find(3)
2441 2441 issue.attachments.delete_all
2442 2442 get :new, :project_id => 1, :copy_from => 3
2443 2443
2444 2444 assert_select 'input[name=copy_attachments]', 0
2445 2445 end
2446 2446
2447 2447 def test_new_as_copy_with_subtasks_should_show_copy_subtasks_checkbox
2448 2448 @request.session[:user_id] = 2
2449 2449 issue = Issue.generate_with_descendants!
2450 2450 get :new, :project_id => 1, :copy_from => issue.id
2451 2451
2452 2452 assert_select 'input[type=checkbox][name=copy_subtasks][checked=checked][value=1]'
2453 2453 end
2454 2454
2455 2455 def test_new_as_copy_with_invalid_issue_should_respond_with_404
2456 2456 @request.session[:user_id] = 2
2457 2457 get :new, :project_id => 1, :copy_from => 99999
2458 2458 assert_response 404
2459 2459 end
2460 2460
2461 2461 def test_create_as_copy_on_different_project
2462 2462 @request.session[:user_id] = 2
2463 2463 assert_difference 'Issue.count' do
2464 2464 post :create, :project_id => 1, :copy_from => 1,
2465 2465 :issue => {:project_id => '2', :tracker_id => '3', :status_id => '1', :subject => 'Copy'}
2466 2466
2467 2467 assert_not_nil assigns(:issue)
2468 2468 assert assigns(:issue).copy?
2469 2469 end
2470 2470 issue = Issue.order('id DESC').first
2471 2471 assert_redirected_to "/issues/#{issue.id}"
2472 2472
2473 2473 assert_equal 2, issue.project_id
2474 2474 assert_equal 3, issue.tracker_id
2475 2475 assert_equal 'Copy', issue.subject
2476 2476 end
2477 2477
2478 2478 def test_create_as_copy_should_copy_attachments
2479 2479 @request.session[:user_id] = 2
2480 2480 issue = Issue.find(3)
2481 2481 count = issue.attachments.count
2482 2482 assert count > 0
2483 2483 assert_difference 'Issue.count' do
2484 2484 assert_difference 'Attachment.count', count do
2485 2485 assert_difference 'Journal.count', 2 do
2486 2486 post :create, :project_id => 1, :copy_from => 3,
2487 2487 :issue => {:project_id => '1', :tracker_id => '3',
2488 2488 :status_id => '1', :subject => 'Copy with attachments'},
2489 2489 :copy_attachments => '1'
2490 2490 end
2491 2491 end
2492 2492 end
2493 2493 copy = Issue.order('id DESC').first
2494 2494 assert_equal count, copy.attachments.count
2495 2495 assert_equal issue.attachments.map(&:filename).sort, copy.attachments.map(&:filename).sort
2496 2496 end
2497 2497
2498 2498 def test_create_as_copy_without_copy_attachments_option_should_not_copy_attachments
2499 2499 @request.session[:user_id] = 2
2500 2500 issue = Issue.find(3)
2501 2501 count = issue.attachments.count
2502 2502 assert count > 0
2503 2503 assert_difference 'Issue.count' do
2504 2504 assert_no_difference 'Attachment.count' do
2505 2505 assert_difference 'Journal.count', 2 do
2506 2506 post :create, :project_id => 1, :copy_from => 3,
2507 2507 :issue => {:project_id => '1', :tracker_id => '3',
2508 2508 :status_id => '1', :subject => 'Copy with attachments'}
2509 2509 end
2510 2510 end
2511 2511 end
2512 2512 copy = Issue.order('id DESC').first
2513 2513 assert_equal 0, copy.attachments.count
2514 2514 end
2515 2515
2516 2516 def test_create_as_copy_with_attachments_should_add_new_files
2517 2517 @request.session[:user_id] = 2
2518 2518 issue = Issue.find(3)
2519 2519 count = issue.attachments.count
2520 2520 assert count > 0
2521 2521 assert_difference 'Issue.count' do
2522 2522 assert_difference 'Attachment.count', count + 1 do
2523 2523 assert_difference 'Journal.count', 2 do
2524 2524 post :create, :project_id => 1, :copy_from => 3,
2525 2525 :issue => {:project_id => '1', :tracker_id => '3',
2526 2526 :status_id => '1', :subject => 'Copy with attachments'},
2527 2527 :copy_attachments => '1',
2528 2528 :attachments => {'1' =>
2529 2529 {'file' => uploaded_test_file('testfile.txt', 'text/plain'),
2530 2530 'description' => 'test file'}}
2531 2531 end
2532 2532 end
2533 2533 end
2534 2534 copy = Issue.order('id DESC').first
2535 2535 assert_equal count + 1, copy.attachments.count
2536 2536 end
2537 2537
2538 2538 def test_create_as_copy_should_add_relation_with_copied_issue
2539 2539 @request.session[:user_id] = 2
2540 2540 assert_difference 'Issue.count' do
2541 2541 assert_difference 'IssueRelation.count' do
2542 2542 post :create, :project_id => 1, :copy_from => 1,
2543 2543 :issue => {:project_id => '1', :tracker_id => '3',
2544 2544 :status_id => '1', :subject => 'Copy'}
2545 2545 end
2546 2546 end
2547 2547 copy = Issue.order('id DESC').first
2548 2548 assert_equal 1, copy.relations.size
2549 2549 end
2550 2550
2551 2551 def test_create_as_copy_should_copy_subtasks
2552 2552 @request.session[:user_id] = 2
2553 2553 issue = Issue.generate_with_descendants!
2554 2554 count = issue.descendants.count
2555 2555 assert_difference 'Issue.count', count + 1 do
2556 2556 assert_difference 'Journal.count', (count + 1) * 2 do
2557 2557 post :create, :project_id => 1, :copy_from => issue.id,
2558 2558 :issue => {:project_id => '1', :tracker_id => '3',
2559 2559 :status_id => '1', :subject => 'Copy with subtasks'},
2560 2560 :copy_subtasks => '1'
2561 2561 end
2562 2562 end
2563 2563 copy = Issue.where(:parent_id => nil).order('id DESC').first
2564 2564 assert_equal count, copy.descendants.count
2565 2565 assert_equal issue.descendants.map(&:subject).sort, copy.descendants.map(&:subject).sort
2566 2566 end
2567 2567
2568 2568 def test_create_as_copy_without_copy_subtasks_option_should_not_copy_subtasks
2569 2569 @request.session[:user_id] = 2
2570 2570 issue = Issue.generate_with_descendants!
2571 2571 assert_difference 'Issue.count', 1 do
2572 2572 assert_difference 'Journal.count', 2 do
2573 2573 post :create, :project_id => 1, :copy_from => 3,
2574 2574 :issue => {:project_id => '1', :tracker_id => '3',
2575 2575 :status_id => '1', :subject => 'Copy with subtasks'}
2576 2576 end
2577 2577 end
2578 2578 copy = Issue.where(:parent_id => nil).order('id DESC').first
2579 2579 assert_equal 0, copy.descendants.count
2580 2580 end
2581 2581
2582 2582 def test_create_as_copy_with_failure
2583 2583 @request.session[:user_id] = 2
2584 2584 post :create, :project_id => 1, :copy_from => 1,
2585 2585 :issue => {:project_id => '2', :tracker_id => '3', :status_id => '1', :subject => ''}
2586 2586
2587 2587 assert_response :success
2588 2588 assert_template 'new'
2589 2589
2590 2590 assert_not_nil assigns(:issue)
2591 2591 assert assigns(:issue).copy?
2592 2592
2593 2593 assert_select 'form#issue-form[action=/projects/ecookbook/issues]' do
2594 2594 assert_select 'select[name=?]', 'issue[project_id]' do
2595 2595 assert_select 'option[value=1]:not([selected])', :text => 'eCookbook'
2596 2596 assert_select 'option[value=2][selected=selected]', :text => 'OnlineStore'
2597 2597 end
2598 2598 assert_select 'input[name=copy_from][value=1]'
2599 2599 end
2600 2600 end
2601 2601
2602 2602 def test_create_as_copy_on_project_without_permission_should_ignore_target_project
2603 2603 @request.session[:user_id] = 2
2604 2604 assert !User.find(2).member_of?(Project.find(4))
2605 2605
2606 2606 assert_difference 'Issue.count' do
2607 2607 post :create, :project_id => 1, :copy_from => 1,
2608 2608 :issue => {:project_id => '4', :tracker_id => '3', :status_id => '1', :subject => 'Copy'}
2609 2609 end
2610 2610 issue = Issue.order('id DESC').first
2611 2611 assert_equal 1, issue.project_id
2612 2612 end
2613 2613
2614 2614 def test_get_edit
2615 2615 @request.session[:user_id] = 2
2616 2616 get :edit, :id => 1
2617 2617 assert_response :success
2618 2618 assert_template 'edit'
2619 2619 assert_not_nil assigns(:issue)
2620 2620 assert_equal Issue.find(1), assigns(:issue)
2621 2621
2622 2622 # Be sure we don't display inactive IssuePriorities
2623 2623 assert ! IssuePriority.find(15).active?
2624 2624 assert_select 'select[name=?]', 'issue[priority_id]' do
2625 2625 assert_select 'option[value=15]', 0
2626 2626 end
2627 2627 end
2628 2628
2629 2629 def test_get_edit_should_display_the_time_entry_form_with_log_time_permission
2630 2630 @request.session[:user_id] = 2
2631 2631 Role.find_by_name('Manager').update_attribute :permissions, [:view_issues, :edit_issues, :log_time]
2632 2632
2633 2633 get :edit, :id => 1
2634 2634 assert_select 'input[name=?]', 'time_entry[hours]'
2635 2635 end
2636 2636
2637 2637 def test_get_edit_should_not_display_the_time_entry_form_without_log_time_permission
2638 2638 @request.session[:user_id] = 2
2639 2639 Role.find_by_name('Manager').remove_permission! :log_time
2640 2640
2641 2641 get :edit, :id => 1
2642 2642 assert_select 'input[name=?]', 'time_entry[hours]', 0
2643 2643 end
2644 2644
2645 2645 def test_get_edit_with_params
2646 2646 @request.session[:user_id] = 2
2647 2647 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 },
2648 2648 :time_entry => { :hours => '2.5', :comments => 'test_get_edit_with_params', :activity_id => 10 }
2649 2649 assert_response :success
2650 2650 assert_template 'edit'
2651 2651
2652 2652 issue = assigns(:issue)
2653 2653 assert_not_nil issue
2654 2654
2655 2655 assert_equal 5, issue.status_id
2656 2656 assert_select 'select[name=?]', 'issue[status_id]' do
2657 2657 assert_select 'option[value=5][selected=selected]', :text => 'Closed'
2658 2658 end
2659 2659
2660 2660 assert_equal 7, issue.priority_id
2661 2661 assert_select 'select[name=?]', 'issue[priority_id]' do
2662 2662 assert_select 'option[value=7][selected=selected]', :text => 'Urgent'
2663 2663 end
2664 2664
2665 2665 assert_select 'input[name=?][value=2.5]', 'time_entry[hours]'
2666 2666 assert_select 'select[name=?]', 'time_entry[activity_id]' do
2667 2667 assert_select 'option[value=10][selected=selected]', :text => 'Development'
2668 2668 end
2669 2669 assert_select 'input[name=?][value=test_get_edit_with_params]', 'time_entry[comments]'
2670 2670 end
2671 2671
2672 2672 def test_get_edit_with_multi_custom_field
2673 2673 field = CustomField.find(1)
2674 2674 field.update_attribute :multiple, true
2675 2675 issue = Issue.find(1)
2676 2676 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
2677 2677 issue.save!
2678 2678
2679 2679 @request.session[:user_id] = 2
2680 2680 get :edit, :id => 1
2681 2681 assert_response :success
2682 2682 assert_template 'edit'
2683 2683
2684 2684 assert_select 'select[name=?][multiple=multiple]', 'issue[custom_field_values][1][]' do
2685 2685 assert_select 'option', 3
2686 2686 assert_select 'option[value=MySQL][selected=selected]'
2687 2687 assert_select 'option[value=Oracle][selected=selected]'
2688 2688 assert_select 'option[value=PostgreSQL]:not([selected])'
2689 2689 end
2690 2690 end
2691 2691
2692 2692 def test_update_form_for_existing_issue
2693 2693 @request.session[:user_id] = 2
2694 2694 xhr :put, :update_form, :project_id => 1,
2695 2695 :id => 1,
2696 2696 :issue => {:tracker_id => 2,
2697 2697 :subject => 'This is the test_new issue',
2698 2698 :description => 'This is the description',
2699 2699 :priority_id => 5}
2700 2700 assert_response :success
2701 2701 assert_equal 'text/javascript', response.content_type
2702 2702 assert_template 'update_form'
2703 2703 assert_template :partial => '_form'
2704 2704
2705 2705 issue = assigns(:issue)
2706 2706 assert_kind_of Issue, issue
2707 2707 assert_equal 1, issue.id
2708 2708 assert_equal 1, issue.project_id
2709 2709 assert_equal 2, issue.tracker_id
2710 2710 assert_equal 'This is the test_new issue', issue.subject
2711 2711 end
2712 2712
2713 2713 def test_update_form_for_existing_issue_should_keep_issue_author
2714 2714 @request.session[:user_id] = 3
2715 2715 xhr :put, :update_form, :project_id => 1, :id => 1, :issue => {:subject => 'Changed'}
2716 2716 assert_response :success
2717 2717 assert_equal 'text/javascript', response.content_type
2718 2718
2719 2719 issue = assigns(:issue)
2720 2720 assert_equal User.find(2), issue.author
2721 2721 assert_equal 2, issue.author_id
2722 2722 assert_not_equal User.current, issue.author
2723 2723 end
2724 2724
2725 2725 def test_update_form_for_existing_issue_should_propose_transitions_based_on_initial_status
2726 2726 @request.session[:user_id] = 2
2727 2727 WorkflowTransition.delete_all
2728 2728 WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 1)
2729 2729 WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 5)
2730 2730 WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 5, :new_status_id => 4)
2731 2731
2732 2732 xhr :put, :update_form, :project_id => 1,
2733 2733 :id => 2,
2734 2734 :issue => {:tracker_id => 2,
2735 2735 :status_id => 5,
2736 2736 :subject => 'This is an issue'}
2737 2737
2738 2738 assert_equal 5, assigns(:issue).status_id
2739 2739 assert_equal [1,2,5], assigns(:allowed_statuses).map(&:id).sort
2740 2740 end
2741 2741
2742 2742 def test_update_form_for_existing_issue_with_project_change
2743 2743 @request.session[:user_id] = 2
2744 2744 xhr :put, :update_form, :project_id => 1,
2745 2745 :id => 1,
2746 2746 :issue => {:project_id => 2,
2747 2747 :tracker_id => 2,
2748 2748 :subject => 'This is the test_new issue',
2749 2749 :description => 'This is the description',
2750 2750 :priority_id => 5}
2751 2751 assert_response :success
2752 2752 assert_template :partial => '_form'
2753 2753
2754 2754 issue = assigns(:issue)
2755 2755 assert_kind_of Issue, issue
2756 2756 assert_equal 1, issue.id
2757 2757 assert_equal 2, issue.project_id
2758 2758 assert_equal 2, issue.tracker_id
2759 2759 assert_equal 'This is the test_new issue', issue.subject
2760 2760 end
2761 2761
2762 2762 def test_update_form_should_propose_default_status_for_existing_issue
2763 2763 @request.session[:user_id] = 2
2764 2764 WorkflowTransition.delete_all
2765 2765 WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 3)
2766 2766
2767 2767 xhr :put, :update_form, :project_id => 1, :id => 2
2768 2768 assert_response :success
2769 2769 assert_equal [2,3], assigns(:allowed_statuses).map(&:id).sort
2770 2770 end
2771 2771
2772 2772 def test_put_update_without_custom_fields_param
2773 2773 @request.session[:user_id] = 2
2774 2774 ActionMailer::Base.deliveries.clear
2775 2775
2776 2776 issue = Issue.find(1)
2777 2777 assert_equal '125', issue.custom_value_for(2).value
2778 2778 old_subject = issue.subject
2779 2779 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
2780 2780
2781 2781 assert_difference('Journal.count') do
2782 2782 assert_difference('JournalDetail.count', 2) do
2783 2783 put :update, :id => 1, :issue => {:subject => new_subject,
2784 2784 :priority_id => '6',
2785 2785 :category_id => '1' # no change
2786 2786 }
2787 2787 end
2788 2788 end
2789 2789 assert_redirected_to :action => 'show', :id => '1'
2790 2790 issue.reload
2791 2791 assert_equal new_subject, issue.subject
2792 2792 # Make sure custom fields were not cleared
2793 2793 assert_equal '125', issue.custom_value_for(2).value
2794 2794
2795 2795 mail = ActionMailer::Base.deliveries.last
2796 2796 assert_not_nil mail
2797 2797 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
2798 2798 assert_mail_body_match "Subject changed from #{old_subject} to #{new_subject}", mail
2799 2799 end
2800 2800
2801 2801 def test_put_update_with_project_change
2802 2802 @request.session[:user_id] = 2
2803 2803 ActionMailer::Base.deliveries.clear
2804 2804
2805 2805 assert_difference('Journal.count') do
2806 2806 assert_difference('JournalDetail.count', 3) do
2807 2807 put :update, :id => 1, :issue => {:project_id => '2',
2808 2808 :tracker_id => '1', # no change
2809 2809 :priority_id => '6',
2810 2810 :category_id => '3'
2811 2811 }
2812 2812 end
2813 2813 end
2814 2814 assert_redirected_to :action => 'show', :id => '1'
2815 2815 issue = Issue.find(1)
2816 2816 assert_equal 2, issue.project_id
2817 2817 assert_equal 1, issue.tracker_id
2818 2818 assert_equal 6, issue.priority_id
2819 2819 assert_equal 3, issue.category_id
2820 2820
2821 2821 mail = ActionMailer::Base.deliveries.last
2822 2822 assert_not_nil mail
2823 2823 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
2824 2824 assert_mail_body_match "Project changed from eCookbook to OnlineStore", mail
2825 2825 end
2826 2826
2827 2827 def test_put_update_with_tracker_change
2828 2828 @request.session[:user_id] = 2
2829 2829 ActionMailer::Base.deliveries.clear
2830 2830
2831 2831 assert_difference('Journal.count') do
2832 2832 assert_difference('JournalDetail.count', 2) do
2833 2833 put :update, :id => 1, :issue => {:project_id => '1',
2834 2834 :tracker_id => '2',
2835 2835 :priority_id => '6'
2836 2836 }
2837 2837 end
2838 2838 end
2839 2839 assert_redirected_to :action => 'show', :id => '1'
2840 2840 issue = Issue.find(1)
2841 2841 assert_equal 1, issue.project_id
2842 2842 assert_equal 2, issue.tracker_id
2843 2843 assert_equal 6, issue.priority_id
2844 2844 assert_equal 1, issue.category_id
2845 2845
2846 2846 mail = ActionMailer::Base.deliveries.last
2847 2847 assert_not_nil mail
2848 2848 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
2849 2849 assert_mail_body_match "Tracker changed from Bug to Feature request", mail
2850 2850 end
2851 2851
2852 2852 def test_put_update_with_custom_field_change
2853 2853 @request.session[:user_id] = 2
2854 2854 issue = Issue.find(1)
2855 2855 assert_equal '125', issue.custom_value_for(2).value
2856 2856
2857 2857 assert_difference('Journal.count') do
2858 2858 assert_difference('JournalDetail.count', 3) do
2859 2859 put :update, :id => 1, :issue => {:subject => 'Custom field change',
2860 2860 :priority_id => '6',
2861 2861 :category_id => '1', # no change
2862 2862 :custom_field_values => { '2' => 'New custom value' }
2863 2863 }
2864 2864 end
2865 2865 end
2866 2866 assert_redirected_to :action => 'show', :id => '1'
2867 2867 issue.reload
2868 2868 assert_equal 'New custom value', issue.custom_value_for(2).value
2869 2869
2870 2870 mail = ActionMailer::Base.deliveries.last
2871 2871 assert_not_nil mail
2872 2872 assert_mail_body_match "Searchable field changed from 125 to New custom value", mail
2873 2873 end
2874 2874
2875 2875 def test_put_update_with_multi_custom_field_change
2876 2876 field = CustomField.find(1)
2877 2877 field.update_attribute :multiple, true
2878 2878 issue = Issue.find(1)
2879 2879 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
2880 2880 issue.save!
2881 2881
2882 2882 @request.session[:user_id] = 2
2883 2883 assert_difference('Journal.count') do
2884 2884 assert_difference('JournalDetail.count', 3) do
2885 2885 put :update, :id => 1,
2886 2886 :issue => {
2887 2887 :subject => 'Custom field change',
2888 2888 :custom_field_values => { '1' => ['', 'Oracle', 'PostgreSQL'] }
2889 2889 }
2890 2890 end
2891 2891 end
2892 2892 assert_redirected_to :action => 'show', :id => '1'
2893 2893 assert_equal ['Oracle', 'PostgreSQL'], Issue.find(1).custom_field_value(1).sort
2894 2894 end
2895 2895
2896 2896 def test_put_update_with_status_and_assignee_change
2897 2897 issue = Issue.find(1)
2898 2898 assert_equal 1, issue.status_id
2899 2899 @request.session[:user_id] = 2
2900 2900 assert_difference('TimeEntry.count', 0) do
2901 2901 put :update,
2902 2902 :id => 1,
2903 2903 :issue => { :status_id => 2, :assigned_to_id => 3, :notes => 'Assigned to dlopper' },
2904 2904 :time_entry => { :hours => '', :comments => '', :activity_id => TimeEntryActivity.first }
2905 2905 end
2906 2906 assert_redirected_to :action => 'show', :id => '1'
2907 2907 issue.reload
2908 2908 assert_equal 2, issue.status_id
2909 2909 j = Journal.order('id DESC').first
2910 2910 assert_equal 'Assigned to dlopper', j.notes
2911 2911 assert_equal 2, j.details.size
2912 2912
2913 2913 mail = ActionMailer::Base.deliveries.last
2914 2914 assert_mail_body_match "Status changed from New to Assigned", mail
2915 2915 # subject should contain the new status
2916 2916 assert mail.subject.include?("(#{ IssueStatus.find(2).name })")
2917 2917 end
2918 2918
2919 2919 def test_put_update_with_note_only
2920 2920 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
2921 2921 # anonymous user
2922 2922 put :update,
2923 2923 :id => 1,
2924 2924 :issue => { :notes => notes }
2925 2925 assert_redirected_to :action => 'show', :id => '1'
2926 2926 j = Journal.order('id DESC').first
2927 2927 assert_equal notes, j.notes
2928 2928 assert_equal 0, j.details.size
2929 2929 assert_equal User.anonymous, j.user
2930 2930
2931 2931 mail = ActionMailer::Base.deliveries.last
2932 2932 assert_mail_body_match notes, mail
2933 2933 end
2934 2934
2935 2935 def test_put_update_with_private_note_only
2936 2936 notes = 'Private note'
2937 2937 @request.session[:user_id] = 2
2938 2938
2939 2939 assert_difference 'Journal.count' do
2940 2940 put :update, :id => 1, :issue => {:notes => notes, :private_notes => '1'}
2941 2941 assert_redirected_to :action => 'show', :id => '1'
2942 2942 end
2943 2943
2944 2944 j = Journal.order('id DESC').first
2945 2945 assert_equal notes, j.notes
2946 2946 assert_equal true, j.private_notes
2947 2947 end
2948 2948
2949 2949 def test_put_update_with_private_note_and_changes
2950 2950 notes = 'Private note'
2951 2951 @request.session[:user_id] = 2
2952 2952
2953 2953 assert_difference 'Journal.count', 2 do
2954 2954 put :update, :id => 1, :issue => {:subject => 'New subject', :notes => notes, :private_notes => '1'}
2955 2955 assert_redirected_to :action => 'show', :id => '1'
2956 2956 end
2957 2957
2958 2958 j = Journal.order('id DESC').first
2959 2959 assert_equal notes, j.notes
2960 2960 assert_equal true, j.private_notes
2961 2961 assert_equal 0, j.details.count
2962 2962
2963 2963 j = Journal.order('id DESC').offset(1).first
2964 2964 assert_nil j.notes
2965 2965 assert_equal false, j.private_notes
2966 2966 assert_equal 1, j.details.count
2967 2967 end
2968 2968
2969 2969 def test_put_update_with_note_and_spent_time
2970 2970 @request.session[:user_id] = 2
2971 2971 spent_hours_before = Issue.find(1).spent_hours
2972 2972 assert_difference('TimeEntry.count') do
2973 2973 put :update,
2974 2974 :id => 1,
2975 2975 :issue => { :notes => '2.5 hours added' },
2976 2976 :time_entry => { :hours => '2.5', :comments => 'test_put_update_with_note_and_spent_time', :activity_id => TimeEntryActivity.first.id }
2977 2977 end
2978 2978 assert_redirected_to :action => 'show', :id => '1'
2979 2979
2980 2980 issue = Issue.find(1)
2981 2981
2982 2982 j = Journal.order('id DESC').first
2983 2983 assert_equal '2.5 hours added', j.notes
2984 2984 assert_equal 0, j.details.size
2985 2985
2986 2986 t = issue.time_entries.find_by_comments('test_put_update_with_note_and_spent_time')
2987 2987 assert_not_nil t
2988 2988 assert_equal 2.5, t.hours
2989 2989 assert_equal spent_hours_before + 2.5, issue.spent_hours
2990 2990 end
2991 2991
2992 2992 def test_put_update_should_preserve_parent_issue_even_if_not_visible
2993 2993 parent = Issue.generate!(:project_id => 1, :is_private => true)
2994 2994 issue = Issue.generate!(:parent_issue_id => parent.id)
2995 2995 assert !parent.visible?(User.find(3))
2996 2996 @request.session[:user_id] = 3
2997 2997
2998 2998 get :edit, :id => issue.id
2999 2999 assert_select 'input[name=?][value=?]', 'issue[parent_issue_id]', parent.id.to_s
3000 3000
3001 3001 put :update, :id => issue.id, :issue => {:subject => 'New subject', :parent_issue_id => parent.id.to_s}
3002 3002 assert_response 302
3003 3003 assert_equal parent, issue.parent
3004 3004 end
3005 3005
3006 3006 def test_put_update_with_attachment_only
3007 3007 set_tmp_attachments_directory
3008 3008
3009 3009 # Delete all fixtured journals, a race condition can occur causing the wrong
3010 3010 # journal to get fetched in the next find.
3011 3011 Journal.delete_all
3012 3012
3013 3013 # anonymous user
3014 3014 assert_difference 'Attachment.count' do
3015 3015 put :update, :id => 1,
3016 3016 :issue => {:notes => ''},
3017 3017 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
3018 3018 end
3019 3019
3020 3020 assert_redirected_to :action => 'show', :id => '1'
3021 3021 j = Issue.find(1).journals.reorder('id DESC').first
3022 3022 assert j.notes.blank?
3023 3023 assert_equal 1, j.details.size
3024 3024 assert_equal 'testfile.txt', j.details.first.value
3025 3025 assert_equal User.anonymous, j.user
3026 3026
3027 3027 attachment = Attachment.order('id DESC').first
3028 3028 assert_equal Issue.find(1), attachment.container
3029 3029 assert_equal User.anonymous, attachment.author
3030 3030 assert_equal 'testfile.txt', attachment.filename
3031 3031 assert_equal 'text/plain', attachment.content_type
3032 3032 assert_equal 'test file', attachment.description
3033 3033 assert_equal 59, attachment.filesize
3034 3034 assert File.exists?(attachment.diskfile)
3035 3035 assert_equal 59, File.size(attachment.diskfile)
3036 3036
3037 3037 mail = ActionMailer::Base.deliveries.last
3038 3038 assert_mail_body_match 'testfile.txt', mail
3039 3039 end
3040 3040
3041 3041 def test_put_update_with_failure_should_save_attachments
3042 3042 set_tmp_attachments_directory
3043 3043 @request.session[:user_id] = 2
3044 3044
3045 3045 assert_no_difference 'Journal.count' do
3046 3046 assert_difference 'Attachment.count' do
3047 3047 put :update, :id => 1,
3048 3048 :issue => { :subject => '' },
3049 3049 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
3050 3050 assert_response :success
3051 3051 assert_template 'edit'
3052 3052 end
3053 3053 end
3054 3054
3055 3055 attachment = Attachment.order('id DESC').first
3056 3056 assert_equal 'testfile.txt', attachment.filename
3057 3057 assert File.exists?(attachment.diskfile)
3058 3058 assert_nil attachment.container
3059 3059
3060 3060 assert_select 'input[name=?][value=?]', 'attachments[p0][token]', attachment.token
3061 3061 assert_select 'input[name=?][value=?]', 'attachments[p0][filename]', 'testfile.txt'
3062 3062 end
3063 3063
3064 3064 def test_put_update_with_failure_should_keep_saved_attachments
3065 3065 set_tmp_attachments_directory
3066 3066 attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2)
3067 3067 @request.session[:user_id] = 2
3068 3068
3069 3069 assert_no_difference 'Journal.count' do
3070 3070 assert_no_difference 'Attachment.count' do
3071 3071 put :update, :id => 1,
3072 3072 :issue => { :subject => '' },
3073 3073 :attachments => {'p0' => {'token' => attachment.token}}
3074 3074 assert_response :success
3075 3075 assert_template 'edit'
3076 3076 end
3077 3077 end
3078 3078
3079 3079 assert_select 'input[name=?][value=?]', 'attachments[p0][token]', attachment.token
3080 3080 assert_select 'input[name=?][value=?]', 'attachments[p0][filename]', 'testfile.txt'
3081 3081 end
3082 3082
3083 3083 def test_put_update_should_attach_saved_attachments
3084 3084 set_tmp_attachments_directory
3085 3085 attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2)
3086 3086 @request.session[:user_id] = 2
3087 3087
3088 3088 assert_difference 'Journal.count' do
3089 3089 assert_difference 'JournalDetail.count' do
3090 3090 assert_no_difference 'Attachment.count' do
3091 3091 put :update, :id => 1,
3092 3092 :issue => {:notes => 'Attachment added'},
3093 3093 :attachments => {'p0' => {'token' => attachment.token}}
3094 3094 assert_redirected_to '/issues/1'
3095 3095 end
3096 3096 end
3097 3097 end
3098 3098
3099 3099 attachment.reload
3100 3100 assert_equal Issue.find(1), attachment.container
3101 3101
3102 3102 journal = Journal.order('id DESC').first
3103 3103 assert_equal 1, journal.details.size
3104 3104 assert_equal 'testfile.txt', journal.details.first.value
3105 3105 end
3106 3106
3107 3107 def test_put_update_with_attachment_that_fails_to_save
3108 3108 set_tmp_attachments_directory
3109 3109
3110 3110 # Delete all fixtured journals, a race condition can occur causing the wrong
3111 3111 # journal to get fetched in the next find.
3112 3112 Journal.delete_all
3113 3113
3114 3114 # Mock out the unsaved attachment
3115 3115 Attachment.any_instance.stubs(:create).returns(Attachment.new)
3116 3116
3117 3117 # anonymous user
3118 3118 put :update,
3119 3119 :id => 1,
3120 3120 :issue => {:notes => ''},
3121 3121 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
3122 3122 assert_redirected_to :action => 'show', :id => '1'
3123 3123 assert_equal '1 file(s) could not be saved.', flash[:warning]
3124 3124 end
3125 3125
3126 3126 def test_put_update_with_no_change
3127 3127 issue = Issue.find(1)
3128 3128 issue.journals.clear
3129 3129 ActionMailer::Base.deliveries.clear
3130 3130
3131 3131 put :update,
3132 3132 :id => 1,
3133 3133 :issue => {:notes => ''}
3134 3134 assert_redirected_to :action => 'show', :id => '1'
3135 3135
3136 3136 issue.reload
3137 3137 assert issue.journals.empty?
3138 3138 # No email should be sent
3139 3139 assert ActionMailer::Base.deliveries.empty?
3140 3140 end
3141 3141
3142 3142 def test_put_update_should_send_a_notification
3143 3143 @request.session[:user_id] = 2
3144 3144 ActionMailer::Base.deliveries.clear
3145 3145 issue = Issue.find(1)
3146 3146 old_subject = issue.subject
3147 3147 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
3148 3148
3149 3149 put :update, :id => 1, :issue => {:subject => new_subject,
3150 3150 :priority_id => '6',
3151 3151 :category_id => '1' # no change
3152 3152 }
3153 3153 assert_equal 1, ActionMailer::Base.deliveries.size
3154 3154 end
3155 3155
3156 3156 def test_put_update_with_invalid_spent_time_hours_only
3157 3157 @request.session[:user_id] = 2
3158 3158 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
3159 3159
3160 3160 assert_no_difference('Journal.count') do
3161 3161 put :update,
3162 3162 :id => 1,
3163 3163 :issue => {:notes => notes},
3164 3164 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
3165 3165 end
3166 3166 assert_response :success
3167 3167 assert_template 'edit'
3168 3168
3169 3169 assert_error_tag :descendant => {:content => /Activity #{ESCAPED_CANT} be blank/}
3170 3170 assert_select 'textarea[name=?]', 'issue[notes]', :text => notes
3171 3171 assert_select 'input[name=?][value=?]', 'time_entry[hours]', '2z'
3172 3172 end
3173 3173
3174 3174 def test_put_update_with_invalid_spent_time_comments_only
3175 3175 @request.session[:user_id] = 2
3176 3176 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
3177 3177
3178 3178 assert_no_difference('Journal.count') do
3179 3179 put :update,
3180 3180 :id => 1,
3181 3181 :issue => {:notes => notes},
3182 3182 :time_entry => {"comments"=>"this is my comment", "activity_id"=>"", "hours"=>""}
3183 3183 end
3184 3184 assert_response :success
3185 3185 assert_template 'edit'
3186 3186
3187 3187 assert_error_tag :descendant => {:content => /Activity #{ESCAPED_CANT} be blank/}
3188 3188 assert_error_tag :descendant => {:content => /Hours #{ESCAPED_CANT} be blank/}
3189 3189 assert_select 'textarea[name=?]', 'issue[notes]', :text => notes
3190 3190 assert_select 'input[name=?][value=?]', 'time_entry[comments]', 'this is my comment'
3191 3191 end
3192 3192
3193 3193 def test_put_update_should_allow_fixed_version_to_be_set_to_a_subproject
3194 3194 issue = Issue.find(2)
3195 3195 @request.session[:user_id] = 2
3196 3196
3197 3197 put :update,
3198 3198 :id => issue.id,
3199 3199 :issue => {
3200 3200 :fixed_version_id => 4
3201 3201 }
3202 3202
3203 3203 assert_response :redirect
3204 3204 issue.reload
3205 3205 assert_equal 4, issue.fixed_version_id
3206 3206 assert_not_equal issue.project_id, issue.fixed_version.project_id
3207 3207 end
3208 3208
3209 3209 def test_put_update_should_redirect_back_using_the_back_url_parameter
3210 3210 issue = Issue.find(2)
3211 3211 @request.session[:user_id] = 2
3212 3212
3213 3213 put :update,
3214 3214 :id => issue.id,
3215 3215 :issue => {
3216 3216 :fixed_version_id => 4
3217 3217 },
3218 3218 :back_url => '/issues'
3219 3219
3220 3220 assert_response :redirect
3221 3221 assert_redirected_to '/issues'
3222 3222 end
3223 3223
3224 3224 def test_put_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
3225 3225 issue = Issue.find(2)
3226 3226 @request.session[:user_id] = 2
3227 3227
3228 3228 put :update,
3229 3229 :id => issue.id,
3230 3230 :issue => {
3231 3231 :fixed_version_id => 4
3232 3232 },
3233 3233 :back_url => 'http://google.com'
3234 3234
3235 3235 assert_response :redirect
3236 3236 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue.id
3237 3237 end
3238 3238
3239 3239 def test_get_bulk_edit
3240 3240 @request.session[:user_id] = 2
3241 3241 get :bulk_edit, :ids => [1, 2]
3242 3242 assert_response :success
3243 3243 assert_template 'bulk_edit'
3244 3244
3245 3245 assert_select 'ul#bulk-selection' do
3246 3246 assert_select 'li', 2
3247 3247 assert_select 'li a', :text => 'Bug #1'
3248 3248 end
3249 3249
3250 3250 assert_select 'form#bulk_edit_form[action=?]', '/issues/bulk_update' do
3251 3251 assert_select 'input[name=?]', 'ids[]', 2
3252 3252 assert_select 'input[name=?][value=1][type=hidden]', 'ids[]'
3253 3253
3254 3254 assert_select 'select[name=?]', 'issue[project_id]'
3255 3255 assert_select 'input[name=?]', 'issue[parent_issue_id]'
3256 3256
3257 3257 # Project specific custom field, date type
3258 3258 field = CustomField.find(9)
3259 3259 assert !field.is_for_all?
3260 3260 assert_equal 'date', field.field_format
3261 3261 assert_select 'input[name=?]', 'issue[custom_field_values][9]'
3262 3262
3263 3263 # System wide custom field
3264 3264 assert CustomField.find(1).is_for_all?
3265 3265 assert_select 'select[name=?]', 'issue[custom_field_values][1]'
3266 3266
3267 3267 # Be sure we don't display inactive IssuePriorities
3268 3268 assert ! IssuePriority.find(15).active?
3269 3269 assert_select 'select[name=?]', 'issue[priority_id]' do
3270 3270 assert_select 'option[value=15]', 0
3271 3271 end
3272 3272 end
3273 3273 end
3274 3274
3275 3275 def test_get_bulk_edit_on_different_projects
3276 3276 @request.session[:user_id] = 2
3277 3277 get :bulk_edit, :ids => [1, 2, 6]
3278 3278 assert_response :success
3279 3279 assert_template 'bulk_edit'
3280 3280
3281 3281 # Can not set issues from different projects as children of an issue
3282 3282 assert_select 'input[name=?]', 'issue[parent_issue_id]', 0
3283 3283
3284 3284 # Project specific custom field, date type
3285 3285 field = CustomField.find(9)
3286 3286 assert !field.is_for_all?
3287 3287 assert !field.project_ids.include?(Issue.find(6).project_id)
3288 3288 assert_select 'input[name=?]', 'issue[custom_field_values][9]', 0
3289 3289 end
3290 3290
3291 3291 def test_get_bulk_edit_with_user_custom_field
3292 3292 field = IssueCustomField.create!(:name => 'Tester', :field_format => 'user', :is_for_all => true)
3293 3293
3294 3294 @request.session[:user_id] = 2
3295 3295 get :bulk_edit, :ids => [1, 2]
3296 3296 assert_response :success
3297 3297 assert_template 'bulk_edit'
3298 3298
3299 3299 assert_select 'select.user_cf[name=?]', "issue[custom_field_values][#{field.id}]" do
3300 3300 assert_select 'option', Project.find(1).users.count + 2 # "no change" + "none" options
3301 3301 end
3302 3302 end
3303 3303
3304 3304 def test_get_bulk_edit_with_version_custom_field
3305 3305 field = IssueCustomField.create!(:name => 'Affected version', :field_format => 'version', :is_for_all => true)
3306 3306
3307 3307 @request.session[:user_id] = 2
3308 3308 get :bulk_edit, :ids => [1, 2]
3309 3309 assert_response :success
3310 3310 assert_template 'bulk_edit'
3311 3311
3312 3312 assert_select 'select.version_cf[name=?]', "issue[custom_field_values][#{field.id}]" do
3313 3313 assert_select 'option', Project.find(1).shared_versions.count + 2 # "no change" + "none" options
3314 3314 end
3315 3315 end
3316 3316
3317 3317 def test_get_bulk_edit_with_multi_custom_field
3318 3318 field = CustomField.find(1)
3319 3319 field.update_attribute :multiple, true
3320 3320
3321 3321 @request.session[:user_id] = 2
3322 3322 get :bulk_edit, :ids => [1, 2]
3323 3323 assert_response :success
3324 3324 assert_template 'bulk_edit'
3325 3325
3326 3326 assert_select 'select[name=?]', 'issue[custom_field_values][1][]' do
3327 3327 assert_select 'option', field.possible_values.size + 1 # "none" options
3328 3328 end
3329 3329 end
3330 3330
3331 3331 def test_bulk_edit_should_propose_to_clear_text_custom_fields
3332 3332 @request.session[:user_id] = 2
3333 3333 get :bulk_edit, :ids => [1, 3]
3334 3334 assert_select 'input[name=?][value=?]', 'issue[custom_field_values][2]', '__none__'
3335 3335 end
3336 3336
3337 3337 def test_bulk_edit_should_only_propose_statuses_allowed_for_all_issues
3338 3338 WorkflowTransition.delete_all
3339 3339 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
3340 3340 :old_status_id => 1, :new_status_id => 1)
3341 3341 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
3342 3342 :old_status_id => 1, :new_status_id => 3)
3343 3343 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
3344 3344 :old_status_id => 1, :new_status_id => 4)
3345 3345 WorkflowTransition.create!(:role_id => 1, :tracker_id => 2,
3346 3346 :old_status_id => 2, :new_status_id => 1)
3347 3347 WorkflowTransition.create!(:role_id => 1, :tracker_id => 2,
3348 3348 :old_status_id => 2, :new_status_id => 3)
3349 3349 WorkflowTransition.create!(:role_id => 1, :tracker_id => 2,
3350 3350 :old_status_id => 2, :new_status_id => 5)
3351 3351 @request.session[:user_id] = 2
3352 3352 get :bulk_edit, :ids => [1, 2]
3353 3353
3354 3354 assert_response :success
3355 3355 statuses = assigns(:available_statuses)
3356 3356 assert_not_nil statuses
3357 3357 assert_equal [1, 3], statuses.map(&:id).sort
3358 3358
3359 3359 assert_select 'select[name=?]', 'issue[status_id]' do
3360 3360 assert_select 'option', 3 # 2 statuses + "no change" option
3361 3361 end
3362 3362 end
3363 3363
3364 3364 def test_bulk_edit_should_propose_target_project_open_shared_versions
3365 3365 @request.session[:user_id] = 2
3366 3366 post :bulk_edit, :ids => [1, 2, 6], :issue => {:project_id => 1}
3367 3367 assert_response :success
3368 3368 assert_template 'bulk_edit'
3369 3369 assert_equal Project.find(1).shared_versions.open.all.sort, assigns(:versions).sort
3370 3370
3371 3371 assert_select 'select[name=?]', 'issue[fixed_version_id]' do
3372 3372 assert_select 'option', :text => '2.0'
3373 3373 end
3374 3374 end
3375 3375
3376 3376 def test_bulk_edit_should_propose_target_project_categories
3377 3377 @request.session[:user_id] = 2
3378 3378 post :bulk_edit, :ids => [1, 2, 6], :issue => {:project_id => 1}
3379 3379 assert_response :success
3380 3380 assert_template 'bulk_edit'
3381 3381 assert_equal Project.find(1).issue_categories.sort, assigns(:categories).sort
3382 3382
3383 3383 assert_select 'select[name=?]', 'issue[category_id]' do
3384 3384 assert_select 'option', :text => 'Recipes'
3385 3385 end
3386 3386 end
3387 3387
3388 3388 def test_bulk_update
3389 3389 @request.session[:user_id] = 2
3390 3390 # update issues priority
3391 3391 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing',
3392 3392 :issue => {:priority_id => 7,
3393 3393 :assigned_to_id => '',
3394 3394 :custom_field_values => {'2' => ''}}
3395 3395
3396 3396 assert_response 302
3397 3397 # check that the issues were updated
3398 3398 assert_equal [7, 7], Issue.where(:id =>[1, 2]).collect {|i| i.priority.id}
3399 3399
3400 3400 issue = Issue.find(1)
3401 3401 journal = issue.journals.reorder('created_on DESC').first
3402 3402 assert_equal '125', issue.custom_value_for(2).value
3403 3403 assert_equal 'Bulk editing', journal.notes
3404 3404 assert_equal 1, journal.details.size
3405 3405 end
3406 3406
3407 3407 def test_bulk_update_with_group_assignee
3408 3408 group = Group.find(11)
3409 3409 project = Project.find(1)
3410 3410 project.members << Member.new(:principal => group, :roles => [Role.givable.first])
3411 3411
3412 3412 @request.session[:user_id] = 2
3413 3413 # update issues assignee
3414 3414 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing',
3415 3415 :issue => {:priority_id => '',
3416 3416 :assigned_to_id => group.id,
3417 3417 :custom_field_values => {'2' => ''}}
3418 3418
3419 3419 assert_response 302
3420 3420 assert_equal [group, group], Issue.where(:id => [1, 2]).collect {|i| i.assigned_to}
3421 3421 end
3422 3422
3423 3423 def test_bulk_update_on_different_projects
3424 3424 @request.session[:user_id] = 2
3425 3425 # update issues priority
3426 3426 post :bulk_update, :ids => [1, 2, 6], :notes => 'Bulk editing',
3427 3427 :issue => {:priority_id => 7,
3428 3428 :assigned_to_id => '',
3429 3429 :custom_field_values => {'2' => ''}}
3430 3430
3431 3431 assert_response 302
3432 3432 # check that the issues were updated
3433 3433 assert_equal [7, 7, 7], Issue.find([1,2,6]).map(&:priority_id)
3434 3434
3435 3435 issue = Issue.find(1)
3436 3436 journal = issue.journals.reorder('created_on DESC').first
3437 3437 assert_equal '125', issue.custom_value_for(2).value
3438 3438 assert_equal 'Bulk editing', journal.notes
3439 3439 assert_equal 1, journal.details.size
3440 3440 end
3441 3441
3442 3442 def test_bulk_update_on_different_projects_without_rights
3443 3443 @request.session[:user_id] = 3
3444 3444 user = User.find(3)
3445 3445 action = { :controller => "issues", :action => "bulk_update" }
3446 3446 assert user.allowed_to?(action, Issue.find(1).project)
3447 3447 assert ! user.allowed_to?(action, Issue.find(6).project)
3448 3448 post :bulk_update, :ids => [1, 6], :notes => 'Bulk should fail',
3449 3449 :issue => {:priority_id => 7,
3450 3450 :assigned_to_id => '',
3451 3451 :custom_field_values => {'2' => ''}}
3452 3452 assert_response 403
3453 3453 assert_not_equal "Bulk should fail", Journal.last.notes
3454 3454 end
3455 3455
3456 3456 def test_bullk_update_should_send_a_notification
3457 3457 @request.session[:user_id] = 2
3458 3458 ActionMailer::Base.deliveries.clear
3459 3459 post(:bulk_update,
3460 3460 {
3461 3461 :ids => [1, 2],
3462 3462 :notes => 'Bulk editing',
3463 3463 :issue => {
3464 3464 :priority_id => 7,
3465 3465 :assigned_to_id => '',
3466 3466 :custom_field_values => {'2' => ''}
3467 3467 }
3468 3468 })
3469 3469
3470 3470 assert_response 302
3471 3471 assert_equal 2, ActionMailer::Base.deliveries.size
3472 3472 end
3473 3473
3474 3474 def test_bulk_update_project
3475 3475 @request.session[:user_id] = 2
3476 3476 post :bulk_update, :ids => [1, 2], :issue => {:project_id => '2'}
3477 3477 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook'
3478 3478 # Issues moved to project 2
3479 3479 assert_equal 2, Issue.find(1).project_id
3480 3480 assert_equal 2, Issue.find(2).project_id
3481 3481 # No tracker change
3482 3482 assert_equal 1, Issue.find(1).tracker_id
3483 3483 assert_equal 2, Issue.find(2).tracker_id
3484 3484 end
3485 3485
3486 3486 def test_bulk_update_project_on_single_issue_should_follow_when_needed
3487 3487 @request.session[:user_id] = 2
3488 3488 post :bulk_update, :id => 1, :issue => {:project_id => '2'}, :follow => '1'
3489 3489 assert_redirected_to '/issues/1'
3490 3490 end
3491 3491
3492 3492 def test_bulk_update_project_on_multiple_issues_should_follow_when_needed
3493 3493 @request.session[:user_id] = 2
3494 3494 post :bulk_update, :id => [1, 2], :issue => {:project_id => '2'}, :follow => '1'
3495 3495 assert_redirected_to '/projects/onlinestore/issues'
3496 3496 end
3497 3497
3498 3498 def test_bulk_update_tracker
3499 3499 @request.session[:user_id] = 2
3500 3500 post :bulk_update, :ids => [1, 2], :issue => {:tracker_id => '2'}
3501 3501 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook'
3502 3502 assert_equal 2, Issue.find(1).tracker_id
3503 3503 assert_equal 2, Issue.find(2).tracker_id
3504 3504 end
3505 3505
3506 3506 def test_bulk_update_status
3507 3507 @request.session[:user_id] = 2
3508 3508 # update issues priority
3509 3509 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing status',
3510 3510 :issue => {:priority_id => '',
3511 3511 :assigned_to_id => '',
3512 3512 :status_id => '5'}
3513 3513
3514 3514 assert_response 302
3515 3515 issue = Issue.find(1)
3516 3516 assert issue.closed?
3517 3517 end
3518 3518
3519 3519 def test_bulk_update_priority
3520 3520 @request.session[:user_id] = 2
3521 3521 post :bulk_update, :ids => [1, 2], :issue => {:priority_id => 6}
3522 3522
3523 3523 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook'
3524 3524 assert_equal 6, Issue.find(1).priority_id
3525 3525 assert_equal 6, Issue.find(2).priority_id
3526 3526 end
3527 3527
3528 3528 def test_bulk_update_with_notes
3529 3529 @request.session[:user_id] = 2
3530 3530 post :bulk_update, :ids => [1, 2], :notes => 'Moving two issues'
3531 3531
3532 3532 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook'
3533 3533 assert_equal 'Moving two issues', Issue.find(1).journals.sort_by(&:id).last.notes
3534 3534 assert_equal 'Moving two issues', Issue.find(2).journals.sort_by(&:id).last.notes
3535 3535 end
3536 3536
3537 3537 def test_bulk_update_parent_id
3538 3538 IssueRelation.delete_all
3539 3539 @request.session[:user_id] = 2
3540 3540 post :bulk_update, :ids => [1, 3],
3541 3541 :notes => 'Bulk editing parent',
3542 3542 :issue => {:priority_id => '', :assigned_to_id => '',
3543 3543 :status_id => '', :parent_issue_id => '2'}
3544 3544 assert_response 302
3545 3545 parent = Issue.find(2)
3546 3546 assert_equal parent.id, Issue.find(1).parent_id
3547 3547 assert_equal parent.id, Issue.find(3).parent_id
3548 3548 assert_equal [1, 3], parent.children.collect(&:id).sort
3549 3549 end
3550 3550
3551 3551 def test_bulk_update_custom_field
3552 3552 @request.session[:user_id] = 2
3553 3553 # update issues priority
3554 3554 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing custom field',
3555 3555 :issue => {:priority_id => '',
3556 3556 :assigned_to_id => '',
3557 3557 :custom_field_values => {'2' => '777'}}
3558 3558
3559 3559 assert_response 302
3560 3560
3561 3561 issue = Issue.find(1)
3562 3562 journal = issue.journals.reorder('created_on DESC').first
3563 3563 assert_equal '777', issue.custom_value_for(2).value
3564 3564 assert_equal 1, journal.details.size
3565 3565 assert_equal '125', journal.details.first.old_value
3566 3566 assert_equal '777', journal.details.first.value
3567 3567 end
3568 3568
3569 3569 def test_bulk_update_custom_field_to_blank
3570 3570 @request.session[:user_id] = 2
3571 3571 post :bulk_update, :ids => [1, 3], :notes => 'Bulk editing custom field',
3572 3572 :issue => {:priority_id => '',
3573 3573 :assigned_to_id => '',
3574 3574 :custom_field_values => {'1' => '__none__'}}
3575 3575 assert_response 302
3576 3576 assert_equal '', Issue.find(1).custom_field_value(1)
3577 3577 assert_equal '', Issue.find(3).custom_field_value(1)
3578 3578 end
3579 3579
3580 3580 def test_bulk_update_multi_custom_field
3581 3581 field = CustomField.find(1)
3582 3582 field.update_attribute :multiple, true
3583 3583
3584 3584 @request.session[:user_id] = 2
3585 3585 post :bulk_update, :ids => [1, 2, 3], :notes => 'Bulk editing multi custom field',
3586 3586 :issue => {:priority_id => '',
3587 3587 :assigned_to_id => '',
3588 3588 :custom_field_values => {'1' => ['MySQL', 'Oracle']}}
3589 3589
3590 3590 assert_response 302
3591 3591
3592 3592 assert_equal ['MySQL', 'Oracle'], Issue.find(1).custom_field_value(1).sort
3593 3593 assert_equal ['MySQL', 'Oracle'], Issue.find(3).custom_field_value(1).sort
3594 3594 # the custom field is not associated with the issue tracker
3595 3595 assert_nil Issue.find(2).custom_field_value(1)
3596 3596 end
3597 3597
3598 3598 def test_bulk_update_multi_custom_field_to_blank
3599 3599 field = CustomField.find(1)
3600 3600 field.update_attribute :multiple, true
3601 3601
3602 3602 @request.session[:user_id] = 2
3603 3603 post :bulk_update, :ids => [1, 3], :notes => 'Bulk editing multi custom field',
3604 3604 :issue => {:priority_id => '',
3605 3605 :assigned_to_id => '',
3606 3606 :custom_field_values => {'1' => ['__none__']}}
3607 3607 assert_response 302
3608 3608 assert_equal [''], Issue.find(1).custom_field_value(1)
3609 3609 assert_equal [''], Issue.find(3).custom_field_value(1)
3610 3610 end
3611 3611
3612 3612 def test_bulk_update_unassign
3613 3613 assert_not_nil Issue.find(2).assigned_to
3614 3614 @request.session[:user_id] = 2
3615 3615 # unassign issues
3616 3616 post :bulk_update, :ids => [1, 2], :notes => 'Bulk unassigning', :issue => {:assigned_to_id => 'none'}
3617 3617 assert_response 302
3618 3618 # check that the issues were updated
3619 3619 assert_nil Issue.find(2).assigned_to
3620 3620 end
3621 3621
3622 3622 def test_post_bulk_update_should_allow_fixed_version_to_be_set_to_a_subproject
3623 3623 @request.session[:user_id] = 2
3624 3624
3625 3625 post :bulk_update, :ids => [1,2], :issue => {:fixed_version_id => 4}
3626 3626
3627 3627 assert_response :redirect
3628 3628 issues = Issue.find([1,2])
3629 3629 issues.each do |issue|
3630 3630 assert_equal 4, issue.fixed_version_id
3631 3631 assert_not_equal issue.project_id, issue.fixed_version.project_id
3632 3632 end
3633 3633 end
3634 3634
3635 3635 def test_post_bulk_update_should_redirect_back_using_the_back_url_parameter
3636 3636 @request.session[:user_id] = 2
3637 3637 post :bulk_update, :ids => [1,2], :back_url => '/issues'
3638 3638
3639 3639 assert_response :redirect
3640 3640 assert_redirected_to '/issues'
3641 3641 end
3642 3642
3643 3643 def test_post_bulk_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
3644 3644 @request.session[:user_id] = 2
3645 3645 post :bulk_update, :ids => [1,2], :back_url => 'http://google.com'
3646 3646
3647 3647 assert_response :redirect
3648 3648 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => Project.find(1).identifier
3649 3649 end
3650 3650
3651 3651 def test_bulk_update_with_all_failures_should_show_errors
3652 3652 @request.session[:user_id] = 2
3653 3653 post :bulk_update, :ids => [1, 2], :issue => {:start_date => 'foo'}
3654 3654
3655 3655 assert_response :success
3656 3656 assert_template 'bulk_edit'
3657 3657 assert_select '#errorExplanation span', :text => 'Failed to save 2 issue(s) on 2 selected: #1, #2.'
3658 3658 assert_select '#errorExplanation ul li', :text => 'Start date is not a valid date: #1, #2'
3659 3659
3660 3660 assert_equal [1, 2], assigns[:issues].map(&:id)
3661 3661 end
3662 3662
3663 3663 def test_bulk_update_with_some_failures_should_show_errors
3664 3664 issue1 = Issue.generate!(:start_date => '2013-05-12')
3665 3665 issue2 = Issue.generate!(:start_date => '2013-05-15')
3666 3666 issue3 = Issue.generate!
3667 3667 @request.session[:user_id] = 2
3668 3668 post :bulk_update, :ids => [issue1.id, issue2.id, issue3.id],
3669 3669 :issue => {:due_date => '2013-05-01'}
3670 3670 assert_response :success
3671 3671 assert_template 'bulk_edit'
3672 3672 assert_select '#errorExplanation span',
3673 3673 :text => "Failed to save 2 issue(s) on 3 selected: ##{issue1.id}, ##{issue2.id}."
3674 3674 assert_select '#errorExplanation ul li',
3675 3675 :text => "Due date must be greater than start date: ##{issue1.id}, ##{issue2.id}"
3676 3676 assert_equal [issue1.id, issue2.id], assigns[:issues].map(&:id)
3677 3677 end
3678 3678
3679 3679 def test_bulk_update_with_failure_should_preserved_form_values
3680 3680 @request.session[:user_id] = 2
3681 3681 post :bulk_update, :ids => [1, 2], :issue => {:tracker_id => '2', :start_date => 'foo'}
3682 3682
3683 3683 assert_response :success
3684 3684 assert_template 'bulk_edit'
3685 3685 assert_select 'select[name=?]', 'issue[tracker_id]' do
3686 3686 assert_select 'option[value=2][selected=selected]'
3687 3687 end
3688 3688 assert_select 'input[name=?][value=?]', 'issue[start_date]', 'foo'
3689 3689 end
3690 3690
3691 3691 def test_get_bulk_copy
3692 3692 @request.session[:user_id] = 2
3693 3693 get :bulk_edit, :ids => [1, 2, 3], :copy => '1'
3694 3694 assert_response :success
3695 3695 assert_template 'bulk_edit'
3696 3696
3697 3697 issues = assigns(:issues)
3698 3698 assert_not_nil issues
3699 3699 assert_equal [1, 2, 3], issues.map(&:id).sort
3700 3700
3701 3701 assert_select 'input[name=copy_attachments]'
3702 3702 end
3703 3703
3704 3704 def test_bulk_copy_to_another_project
3705 3705 @request.session[:user_id] = 2
3706 3706 assert_difference 'Issue.count', 2 do
3707 3707 assert_no_difference 'Project.find(1).issues.count' do
3708 3708 post :bulk_update, :ids => [1, 2], :issue => {:project_id => '2'}, :copy => '1'
3709 3709 end
3710 3710 end
3711 3711 assert_redirected_to '/projects/ecookbook/issues'
3712 3712
3713 3713 copies = Issue.order('id DESC').limit(issues.size)
3714 3714 copies.each do |copy|
3715 3715 assert_equal 2, copy.project_id
3716 3716 end
3717 3717 end
3718 3718
3719 3719 def test_bulk_copy_should_allow_not_changing_the_issue_attributes
3720 3720 @request.session[:user_id] = 2
3721 3721 issues = [
3722 3722 Issue.create!(:project_id => 1, :tracker_id => 1, :status_id => 1,
3723 3723 :priority_id => 2, :subject => 'issue 1', :author_id => 1,
3724 3724 :assigned_to_id => nil),
3725 3725 Issue.create!(:project_id => 2, :tracker_id => 3, :status_id => 2,
3726 3726 :priority_id => 1, :subject => 'issue 2', :author_id => 2,
3727 3727 :assigned_to_id => 3)
3728 3728 ]
3729 3729 assert_difference 'Issue.count', issues.size do
3730 3730 post :bulk_update, :ids => issues.map(&:id), :copy => '1',
3731 3731 :issue => {
3732 3732 :project_id => '', :tracker_id => '', :assigned_to_id => '',
3733 3733 :status_id => '', :start_date => '', :due_date => ''
3734 3734 }
3735 3735 end
3736 3736
3737 3737 copies = Issue.order('id DESC').limit(issues.size)
3738 3738 issues.each do |orig|
3739 3739 copy = copies.detect {|c| c.subject == orig.subject}
3740 3740 assert_not_nil copy
3741 3741 assert_equal orig.project_id, copy.project_id
3742 3742 assert_equal orig.tracker_id, copy.tracker_id
3743 3743 assert_equal orig.status_id, copy.status_id
3744 3744 assert_equal orig.assigned_to_id, copy.assigned_to_id
3745 3745 assert_equal orig.priority_id, copy.priority_id
3746 3746 end
3747 3747 end
3748 3748
3749 3749 def test_bulk_copy_should_allow_changing_the_issue_attributes
3750 3750 # Fixes random test failure with Mysql
3751 3751 # where Issue.where(:project_id => 2).limit(2).order('id desc')
3752 3752 # doesn't return the expected results
3753 3753 Issue.delete_all("project_id=2")
3754 3754
3755 3755 @request.session[:user_id] = 2
3756 3756 assert_difference 'Issue.count', 2 do
3757 3757 assert_no_difference 'Project.find(1).issues.count' do
3758 3758 post :bulk_update, :ids => [1, 2], :copy => '1',
3759 3759 :issue => {
3760 3760 :project_id => '2', :tracker_id => '', :assigned_to_id => '4',
3761 3761 :status_id => '1', :start_date => '2009-12-01', :due_date => '2009-12-31'
3762 3762 }
3763 3763 end
3764 3764 end
3765 3765
3766 3766 copied_issues = Issue.where(:project_id => 2).limit(2).order('id desc').to_a
3767 3767 assert_equal 2, copied_issues.size
3768 3768 copied_issues.each do |issue|
3769 3769 assert_equal 2, issue.project_id, "Project is incorrect"
3770 3770 assert_equal 4, issue.assigned_to_id, "Assigned to is incorrect"
3771 3771 assert_equal 1, issue.status_id, "Status is incorrect"
3772 3772 assert_equal '2009-12-01', issue.start_date.to_s, "Start date is incorrect"
3773 3773 assert_equal '2009-12-31', issue.due_date.to_s, "Due date is incorrect"
3774 3774 end
3775 3775 end
3776 3776
3777 3777 def test_bulk_copy_should_allow_adding_a_note
3778 3778 @request.session[:user_id] = 2
3779 3779 assert_difference 'Issue.count', 1 do
3780 3780 post :bulk_update, :ids => [1], :copy => '1',
3781 3781 :notes => 'Copying one issue',
3782 3782 :issue => {
3783 3783 :project_id => '', :tracker_id => '', :assigned_to_id => '4',
3784 3784 :status_id => '3', :start_date => '2009-12-01', :due_date => '2009-12-31'
3785 3785 }
3786 3786 end
3787 3787 issue = Issue.order('id DESC').first
3788 3788 assert_equal 1, issue.journals.size
3789 3789 journal = issue.journals.first
3790 3790 assert_equal 1, journal.details.size
3791 3791 assert_equal 'Copying one issue', journal.notes
3792 3792 end
3793 3793
3794 3794 def test_bulk_copy_should_allow_not_copying_the_attachments
3795 3795 attachment_count = Issue.find(3).attachments.size
3796 3796 assert attachment_count > 0
3797 3797 @request.session[:user_id] = 2
3798 3798
3799 3799 assert_difference 'Issue.count', 1 do
3800 3800 assert_no_difference 'Attachment.count' do
3801 3801 post :bulk_update, :ids => [3], :copy => '1',
3802 3802 :issue => {
3803 3803 :project_id => ''
3804 3804 }
3805 3805 end
3806 3806 end
3807 3807 end
3808 3808
3809 3809 def test_bulk_copy_should_allow_copying_the_attachments
3810 3810 attachment_count = Issue.find(3).attachments.size
3811 3811 assert attachment_count > 0
3812 3812 @request.session[:user_id] = 2
3813 3813
3814 3814 assert_difference 'Issue.count', 1 do
3815 3815 assert_difference 'Attachment.count', attachment_count do
3816 3816 post :bulk_update, :ids => [3], :copy => '1', :copy_attachments => '1',
3817 3817 :issue => {
3818 3818 :project_id => ''
3819 3819 }
3820 3820 end
3821 3821 end
3822 3822 end
3823 3823
3824 3824 def test_bulk_copy_should_add_relations_with_copied_issues
3825 3825 @request.session[:user_id] = 2
3826 3826
3827 3827 assert_difference 'Issue.count', 2 do
3828 3828 assert_difference 'IssueRelation.count', 2 do
3829 3829 post :bulk_update, :ids => [1, 3], :copy => '1',
3830 3830 :issue => {
3831 3831 :project_id => '1'
3832 3832 }
3833 3833 end
3834 3834 end
3835 3835 end
3836 3836
3837 3837 def test_bulk_copy_should_allow_not_copying_the_subtasks
3838 3838 issue = Issue.generate_with_descendants!
3839 3839 @request.session[:user_id] = 2
3840 3840
3841 3841 assert_difference 'Issue.count', 1 do
3842 3842 post :bulk_update, :ids => [issue.id], :copy => '1',
3843 3843 :issue => {
3844 3844 :project_id => ''
3845 3845 }
3846 3846 end
3847 3847 end
3848 3848
3849 3849 def test_bulk_copy_should_allow_copying_the_subtasks
3850 3850 issue = Issue.generate_with_descendants!
3851 3851 count = issue.descendants.count
3852 3852 @request.session[:user_id] = 2
3853 3853
3854 3854 assert_difference 'Issue.count', count+1 do
3855 3855 post :bulk_update, :ids => [issue.id], :copy => '1', :copy_subtasks => '1',
3856 3856 :issue => {
3857 3857 :project_id => ''
3858 3858 }
3859 3859 end
3860 3860 copy = Issue.where(:parent_id => nil).order("id DESC").first
3861 3861 assert_equal count, copy.descendants.count
3862 3862 end
3863 3863
3864 3864 def test_bulk_copy_should_not_copy_selected_subtasks_twice
3865 3865 issue = Issue.generate_with_descendants!
3866 3866 count = issue.descendants.count
3867 3867 @request.session[:user_id] = 2
3868 3868
3869 3869 assert_difference 'Issue.count', count+1 do
3870 3870 post :bulk_update, :ids => issue.self_and_descendants.map(&:id), :copy => '1', :copy_subtasks => '1',
3871 3871 :issue => {
3872 3872 :project_id => ''
3873 3873 }
3874 3874 end
3875 3875 copy = Issue.where(:parent_id => nil).order("id DESC").first
3876 3876 assert_equal count, copy.descendants.count
3877 3877 end
3878 3878
3879 3879 def test_bulk_copy_to_another_project_should_follow_when_needed
3880 3880 @request.session[:user_id] = 2
3881 3881 post :bulk_update, :ids => [1], :copy => '1', :issue => {:project_id => 2}, :follow => '1'
3882 3882 issue = Issue.order('id DESC').first
3883 3883 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
3884 3884 end
3885 3885
3886 3886 def test_bulk_copy_with_all_failures_should_display_errors
3887 3887 @request.session[:user_id] = 2
3888 3888 post :bulk_update, :ids => [1, 2], :copy => '1', :issue => {:start_date => 'foo'}
3889 3889
3890 3890 assert_response :success
3891 3891 end
3892 3892
3893 3893 def test_destroy_issue_with_no_time_entries
3894 3894 assert_nil TimeEntry.find_by_issue_id(2)
3895 3895 @request.session[:user_id] = 2
3896 3896
3897 3897 assert_difference 'Issue.count', -1 do
3898 3898 delete :destroy, :id => 2
3899 3899 end
3900 3900 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
3901 3901 assert_nil Issue.find_by_id(2)
3902 3902 end
3903 3903
3904 3904 def test_destroy_issues_with_time_entries
3905 3905 @request.session[:user_id] = 2
3906 3906
3907 3907 assert_no_difference 'Issue.count' do
3908 3908 delete :destroy, :ids => [1, 3]
3909 3909 end
3910 3910 assert_response :success
3911 3911 assert_template 'destroy'
3912 3912 assert_not_nil assigns(:hours)
3913 3913 assert Issue.find_by_id(1) && Issue.find_by_id(3)
3914 3914
3915 3915 assert_select 'form' do
3916 3916 assert_select 'input[name=_method][value=delete]'
3917 3917 end
3918 3918 end
3919 3919
3920 3920 def test_destroy_issues_and_destroy_time_entries
3921 3921 @request.session[:user_id] = 2
3922 3922
3923 3923 assert_difference 'Issue.count', -2 do
3924 3924 assert_difference 'TimeEntry.count', -3 do
3925 3925 delete :destroy, :ids => [1, 3], :todo => 'destroy'
3926 3926 end
3927 3927 end
3928 3928 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
3929 3929 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
3930 3930 assert_nil TimeEntry.find_by_id([1, 2])
3931 3931 end
3932 3932
3933 3933 def test_destroy_issues_and_assign_time_entries_to_project
3934 3934 @request.session[:user_id] = 2
3935 3935
3936 3936 assert_difference 'Issue.count', -2 do
3937 3937 assert_no_difference 'TimeEntry.count' do
3938 3938 delete :destroy, :ids => [1, 3], :todo => 'nullify'
3939 3939 end
3940 3940 end
3941 3941 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
3942 3942 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
3943 3943 assert_nil TimeEntry.find(1).issue_id
3944 3944 assert_nil TimeEntry.find(2).issue_id
3945 3945 end
3946 3946
3947 3947 def test_destroy_issues_and_reassign_time_entries_to_another_issue
3948 3948 @request.session[:user_id] = 2
3949 3949
3950 3950 assert_difference 'Issue.count', -2 do
3951 3951 assert_no_difference 'TimeEntry.count' do
3952 3952 delete :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
3953 3953 end
3954 3954 end
3955 3955 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
3956 3956 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
3957 3957 assert_equal 2, TimeEntry.find(1).issue_id
3958 3958 assert_equal 2, TimeEntry.find(2).issue_id
3959 3959 end
3960 3960
3961 3961 def test_destroy_issues_from_different_projects
3962 3962 @request.session[:user_id] = 2
3963 3963
3964 3964 assert_difference 'Issue.count', -3 do
3965 3965 delete :destroy, :ids => [1, 2, 6], :todo => 'destroy'
3966 3966 end
3967 3967 assert_redirected_to :controller => 'issues', :action => 'index'
3968 3968 assert !(Issue.find_by_id(1) || Issue.find_by_id(2) || Issue.find_by_id(6))
3969 3969 end
3970 3970
3971 3971 def test_destroy_parent_and_child_issues
3972 3972 parent = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'Parent Issue')
3973 3973 child = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'Child Issue', :parent_issue_id => parent.id)
3974 3974 assert child.is_descendant_of?(parent.reload)
3975 3975
3976 3976 @request.session[:user_id] = 2
3977 3977 assert_difference 'Issue.count', -2 do
3978 3978 delete :destroy, :ids => [parent.id, child.id], :todo => 'destroy'
3979 3979 end
3980 3980 assert_response 302
3981 3981 end
3982 3982
3983 3983 def test_destroy_invalid_should_respond_with_404
3984 3984 @request.session[:user_id] = 2
3985 3985 assert_no_difference 'Issue.count' do
3986 3986 delete :destroy, :id => 999
3987 3987 end
3988 3988 assert_response 404
3989 3989 end
3990 3990
3991 3991 def test_default_search_scope
3992 3992 get :index
3993 3993
3994 3994 assert_select 'div#quick-search form' do
3995 3995 assert_select 'input[name=issues][value=1][type=hidden]'
3996 3996 end
3997 3997 end
3998 3998 end
@@ -1,85 +1,94
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2014 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
20 20 class ProjectsHelperTest < ActionView::TestCase
21 21 include ApplicationHelper
22 22 include ProjectsHelper
23 23 include Redmine::I18n
24 24 include ERB::Util
25 25 include Rails.application.routes.url_helpers
26 26
27 27 fixtures :projects, :trackers, :issue_statuses, :issues,
28 28 :enumerations, :users, :issue_categories,
29 29 :versions,
30 30 :projects_trackers,
31 31 :member_roles,
32 32 :members,
33 33 :groups_users,
34 34 :enabled_modules
35 35
36 36 def setup
37 37 super
38 38 set_language_if_valid('en')
39 39 User.current = nil
40 40 end
41 41
42 42 def test_link_to_version_within_project
43 43 @project = Project.find(2)
44 44 User.current = User.find(1)
45 45 assert_equal '<a href="/versions/5" title="07/01/2006">Alpha</a>', link_to_version(Version.find(5))
46 46 end
47 47
48 48 def test_link_to_version
49 49 User.current = User.find(1)
50 assert_equal '<a href="/versions/5" title="07/01/2006">OnlineStore - Alpha</a>', link_to_version(Version.find(5))
50 assert_equal '<a href="/versions/5" title="07/01/2006">Alpha</a>', link_to_version(Version.find(5))
51 51 end
52 52
53 53 def test_link_to_version_without_effective_date
54 54 User.current = User.find(1)
55 55 version = Version.find(5)
56 56 version.effective_date = nil
57 assert_equal '<a href="/versions/5">OnlineStore - Alpha</a>', link_to_version(version)
57 assert_equal '<a href="/versions/5">Alpha</a>', link_to_version(version)
58 58 end
59 59
60 60 def test_link_to_private_version
61 assert_equal 'OnlineStore - Alpha', link_to_version(Version.find(5))
61 assert_equal 'Alpha', link_to_version(Version.find(5))
62 62 end
63 63
64 64 def test_link_to_version_invalid_version
65 65 assert_equal '', link_to_version(Object)
66 66 end
67 67
68 68 def test_format_version_name_within_project
69 69 @project = Project.find(1)
70 70 assert_equal "0.1", format_version_name(Version.find(1))
71 71 end
72 72
73 73 def test_format_version_name
74 assert_equal "eCookbook - 0.1", format_version_name(Version.find(1))
74 assert_equal "0.1", format_version_name(Version.find(1))
75 end
76
77 def test_format_version_name_for_shared_version_within_project_should_not_display_project_name
78 @project = Project.find(1)
79 version = Version.find(1)
80 version.sharing = 'system'
81 assert_equal "0.1", format_version_name(version)
75 82 end
76 83
77 def test_format_version_name_for_system_version
78 assert_equal "OnlineStore - Systemwide visible version", format_version_name(Version.find(7))
84 def test_format_version_name_for_shared_version_should_display_project_name
85 version = Version.find(1)
86 version.sharing = 'system'
87 assert_equal "eCookbook - 0.1", format_version_name(version)
79 88 end
80 89
81 90 def test_version_options_for_select_with_no_versions
82 91 assert_equal '', version_options_for_select([])
83 92 assert_equal '', version_options_for_select([], Version.find(1))
84 93 end
85 94 end
General Comments 0
You need to be logged in to leave comments. Login now