##// END OF EJS Templates
Fixed that the sidebar may be displayed empty (#15414)....
Jean-Philippe Lang -
r12079:bba304ef3907
parent child
Show More
@@ -1,1274 +1,1269
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2013 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 = truncate(issue.subject, :length => 60)
76 76 else
77 77 subject = issue.subject
78 78 if options[:truncate]
79 79 subject = truncate(subject, :length => options[:truncate])
80 80 end
81 81 end
82 82 s = link_to text, issue_path(issue), :class => issue.css_classes, :title => title
83 83 s << h(": #{subject}") if subject
84 84 s = h("#{issue.project} - ") + s if options[:project]
85 85 s
86 86 end
87 87
88 88 # Generates a link to an attachment.
89 89 # Options:
90 90 # * :text - Link text (default to attachment filename)
91 91 # * :download - Force download (default: false)
92 92 def link_to_attachment(attachment, options={})
93 93 text = options.delete(:text) || attachment.filename
94 94 route_method = options.delete(:download) ? :download_named_attachment_path : :named_attachment_path
95 95 html_options = options.slice!(:only_path)
96 96 url = send(route_method, attachment, attachment.filename, options)
97 97 link_to text, url, html_options
98 98 end
99 99
100 100 # Generates a link to a SCM revision
101 101 # Options:
102 102 # * :text - Link text (default to the formatted revision)
103 103 def link_to_revision(revision, repository, options={})
104 104 if repository.is_a?(Project)
105 105 repository = repository.repository
106 106 end
107 107 text = options.delete(:text) || format_revision(revision)
108 108 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
109 109 link_to(
110 110 h(text),
111 111 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
112 112 :title => l(:label_revision_id, format_revision(revision))
113 113 )
114 114 end
115 115
116 116 # Generates a link to a message
117 117 def link_to_message(message, options={}, html_options = nil)
118 118 link_to(
119 119 truncate(message.subject, :length => 60),
120 120 board_message_path(message.board_id, message.parent_id || message.id, {
121 121 :r => (message.parent_id && message.id),
122 122 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
123 123 }.merge(options)),
124 124 html_options
125 125 )
126 126 end
127 127
128 128 # Generates a link to a project if active
129 129 # Examples:
130 130 #
131 131 # link_to_project(project) # => link to the specified project overview
132 132 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
133 133 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
134 134 #
135 135 def link_to_project(project, options={}, html_options = nil)
136 136 if project.archived?
137 137 h(project.name)
138 138 elsif options.key?(:action)
139 139 ActiveSupport::Deprecation.warn "#link_to_project with :action option is deprecated and will be removed in Redmine 3.0."
140 140 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
141 141 link_to project.name, url, html_options
142 142 else
143 143 link_to project.name, project_path(project, options), html_options
144 144 end
145 145 end
146 146
147 147 # Generates a link to a project settings if active
148 148 def link_to_project_settings(project, options={}, html_options=nil)
149 149 if project.active?
150 150 link_to project.name, settings_project_path(project, options), html_options
151 151 elsif project.archived?
152 152 h(project.name)
153 153 else
154 154 link_to project.name, project_path(project, options), html_options
155 155 end
156 156 end
157 157
158 158 def wiki_page_path(page, options={})
159 159 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
160 160 end
161 161
162 162 def thumbnail_tag(attachment)
163 163 link_to image_tag(thumbnail_path(attachment)),
164 164 named_attachment_path(attachment, attachment.filename),
165 165 :title => attachment.filename
166 166 end
167 167
168 168 def toggle_link(name, id, options={})
169 169 onclick = "$('##{id}').toggle(); "
170 170 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
171 171 onclick << "return false;"
172 172 link_to(name, "#", :onclick => onclick)
173 173 end
174 174
175 175 def image_to_function(name, function, html_options = {})
176 176 html_options.symbolize_keys!
177 177 tag(:input, html_options.merge({
178 178 :type => "image", :src => image_path(name),
179 179 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
180 180 }))
181 181 end
182 182
183 183 def format_activity_title(text)
184 184 h(truncate_single_line(text, :length => 100))
185 185 end
186 186
187 187 def format_activity_day(date)
188 188 date == User.current.today ? l(:label_today).titleize : format_date(date)
189 189 end
190 190
191 191 def format_activity_description(text)
192 192 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
193 193 ).gsub(/[\r\n]+/, "<br />").html_safe
194 194 end
195 195
196 196 def format_version_name(version)
197 197 if version.project == @project
198 198 h(version)
199 199 else
200 200 h("#{version.project} - #{version}")
201 201 end
202 202 end
203 203
204 204 def due_date_distance_in_words(date)
205 205 if date
206 206 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
207 207 end
208 208 end
209 209
210 210 # Renders a tree of projects as a nested set of unordered lists
211 211 # The given collection may be a subset of the whole project tree
212 212 # (eg. some intermediate nodes are private and can not be seen)
213 213 def render_project_nested_lists(projects)
214 214 s = ''
215 215 if projects.any?
216 216 ancestors = []
217 217 original_project = @project
218 218 projects.sort_by(&:lft).each do |project|
219 219 # set the project environment to please macros.
220 220 @project = project
221 221 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
222 222 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
223 223 else
224 224 ancestors.pop
225 225 s << "</li>"
226 226 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
227 227 ancestors.pop
228 228 s << "</ul></li>\n"
229 229 end
230 230 end
231 231 classes = (ancestors.empty? ? 'root' : 'child')
232 232 s << "<li class='#{classes}'><div class='#{classes}'>"
233 233 s << h(block_given? ? yield(project) : project.name)
234 234 s << "</div>\n"
235 235 ancestors << project
236 236 end
237 237 s << ("</li></ul>\n" * ancestors.size)
238 238 @project = original_project
239 239 end
240 240 s.html_safe
241 241 end
242 242
243 243 def render_page_hierarchy(pages, node=nil, options={})
244 244 content = ''
245 245 if pages[node]
246 246 content << "<ul class=\"pages-hierarchy\">\n"
247 247 pages[node].each do |page|
248 248 content << "<li>"
249 249 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
250 250 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
251 251 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
252 252 content << "</li>\n"
253 253 end
254 254 content << "</ul>\n"
255 255 end
256 256 content.html_safe
257 257 end
258 258
259 259 # Renders flash messages
260 260 def render_flash_messages
261 261 s = ''
262 262 flash.each do |k,v|
263 263 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
264 264 end
265 265 s.html_safe
266 266 end
267 267
268 268 # Renders tabs and their content
269 269 def render_tabs(tabs)
270 270 if tabs.any?
271 271 render :partial => 'common/tabs', :locals => {:tabs => tabs}
272 272 else
273 273 content_tag 'p', l(:label_no_data), :class => "nodata"
274 274 end
275 275 end
276 276
277 277 # Renders the project quick-jump box
278 278 def render_project_jump_box
279 279 return unless User.current.logged?
280 280 projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
281 281 if projects.any?
282 282 options =
283 283 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
284 284 '<option value="" disabled="disabled">---</option>').html_safe
285 285
286 286 options << project_tree_options_for_select(projects, :selected => @project) do |p|
287 287 { :value => project_path(:id => p, :jump => current_menu_item) }
288 288 end
289 289
290 290 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
291 291 end
292 292 end
293 293
294 294 def project_tree_options_for_select(projects, options = {})
295 295 s = ''
296 296 project_tree(projects) do |project, level|
297 297 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
298 298 tag_options = {:value => project.id}
299 299 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
300 300 tag_options[:selected] = 'selected'
301 301 else
302 302 tag_options[:selected] = nil
303 303 end
304 304 tag_options.merge!(yield(project)) if block_given?
305 305 s << content_tag('option', name_prefix + h(project), tag_options)
306 306 end
307 307 s.html_safe
308 308 end
309 309
310 310 # Yields the given block for each project with its level in the tree
311 311 #
312 312 # Wrapper for Project#project_tree
313 313 def project_tree(projects, &block)
314 314 Project.project_tree(projects, &block)
315 315 end
316 316
317 317 def principals_check_box_tags(name, principals)
318 318 s = ''
319 319 principals.each do |principal|
320 320 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
321 321 end
322 322 s.html_safe
323 323 end
324 324
325 325 # Returns a string for users/groups option tags
326 326 def principals_options_for_select(collection, selected=nil)
327 327 s = ''
328 328 if collection.include?(User.current)
329 329 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
330 330 end
331 331 groups = ''
332 332 collection.sort.each do |element|
333 333 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
334 334 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
335 335 end
336 336 unless groups.empty?
337 337 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
338 338 end
339 339 s.html_safe
340 340 end
341 341
342 342 # Options for the new membership projects combo-box
343 343 def options_for_membership_project_select(principal, projects)
344 344 options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
345 345 options << project_tree_options_for_select(projects) do |p|
346 346 {:disabled => principal.projects.to_a.include?(p)}
347 347 end
348 348 options
349 349 end
350 350
351 351 def option_tag(name, text, value, selected=nil, options={})
352 352 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
353 353 end
354 354
355 355 # Truncates and returns the string as a single line
356 356 def truncate_single_line(string, *args)
357 357 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
358 358 end
359 359
360 360 # Truncates at line break after 250 characters or options[:length]
361 361 def truncate_lines(string, options={})
362 362 length = options[:length] || 250
363 363 if string.to_s =~ /\A(.{#{length}}.*?)$/m
364 364 "#{$1}..."
365 365 else
366 366 string
367 367 end
368 368 end
369 369
370 370 def anchor(text)
371 371 text.to_s.gsub(' ', '_')
372 372 end
373 373
374 374 def html_hours(text)
375 375 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
376 376 end
377 377
378 378 def authoring(created, author, options={})
379 379 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
380 380 end
381 381
382 382 def time_tag(time)
383 383 text = distance_of_time_in_words(Time.now, time)
384 384 if @project
385 385 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
386 386 else
387 387 content_tag('abbr', text, :title => format_time(time))
388 388 end
389 389 end
390 390
391 391 def syntax_highlight_lines(name, content)
392 392 lines = []
393 393 syntax_highlight(name, content).each_line { |line| lines << line }
394 394 lines
395 395 end
396 396
397 397 def syntax_highlight(name, content)
398 398 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
399 399 end
400 400
401 401 def to_path_param(path)
402 402 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
403 403 str.blank? ? nil : str
404 404 end
405 405
406 406 def reorder_links(name, url, method = :post)
407 407 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
408 408 url.merge({"#{name}[move_to]" => 'highest'}),
409 409 :method => method, :title => l(:label_sort_highest)) +
410 410 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
411 411 url.merge({"#{name}[move_to]" => 'higher'}),
412 412 :method => method, :title => l(:label_sort_higher)) +
413 413 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
414 414 url.merge({"#{name}[move_to]" => 'lower'}),
415 415 :method => method, :title => l(:label_sort_lower)) +
416 416 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
417 417 url.merge({"#{name}[move_to]" => 'lowest'}),
418 418 :method => method, :title => l(:label_sort_lowest))
419 419 end
420 420
421 421 def breadcrumb(*args)
422 422 elements = args.flatten
423 423 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
424 424 end
425 425
426 426 def other_formats_links(&block)
427 427 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
428 428 yield Redmine::Views::OtherFormatsBuilder.new(self)
429 429 concat('</p>'.html_safe)
430 430 end
431 431
432 432 def page_header_title
433 433 if @project.nil? || @project.new_record?
434 434 h(Setting.app_title)
435 435 else
436 436 b = []
437 437 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
438 438 if ancestors.any?
439 439 root = ancestors.shift
440 440 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
441 441 if ancestors.size > 2
442 442 b << "\xe2\x80\xa6"
443 443 ancestors = ancestors[-2, 2]
444 444 end
445 445 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
446 446 end
447 447 b << h(@project)
448 448 b.join(" \xc2\xbb ").html_safe
449 449 end
450 450 end
451 451
452 452 # Returns a h2 tag and sets the html title with the given arguments
453 453 def title(*args)
454 454 strings = args.map do |arg|
455 455 if arg.is_a?(Array) && arg.size >= 2
456 456 link_to(*arg)
457 457 else
458 458 h(arg.to_s)
459 459 end
460 460 end
461 461 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
462 462 content_tag('h2', strings.join(' &#187; ').html_safe)
463 463 end
464 464
465 465 # Sets the html title
466 466 # Returns the html title when called without arguments
467 467 # Current project name and app_title and automatically appended
468 468 # Exemples:
469 469 # html_title 'Foo', 'Bar'
470 470 # html_title # => 'Foo - Bar - My Project - Redmine'
471 471 def html_title(*args)
472 472 if args.empty?
473 473 title = @html_title || []
474 474 title << @project.name if @project
475 475 title << Setting.app_title unless Setting.app_title == title.last
476 476 title.reject(&:blank?).join(' - ')
477 477 else
478 478 @html_title ||= []
479 479 @html_title += args
480 480 end
481 481 end
482 482
483 483 # Returns the theme, controller name, and action as css classes for the
484 484 # HTML body.
485 485 def body_css_classes
486 486 css = []
487 487 if theme = Redmine::Themes.theme(Setting.ui_theme)
488 488 css << 'theme-' + theme.name
489 489 end
490 490
491 491 css << 'project-' + @project.identifier if @project && @project.identifier.present?
492 492 css << 'controller-' + controller_name
493 493 css << 'action-' + action_name
494 494 css.join(' ')
495 495 end
496 496
497 497 def accesskey(s)
498 498 @used_accesskeys ||= []
499 499 key = Redmine::AccessKeys.key_for(s)
500 500 return nil if @used_accesskeys.include?(key)
501 501 @used_accesskeys << key
502 502 key
503 503 end
504 504
505 505 # Formats text according to system settings.
506 506 # 2 ways to call this method:
507 507 # * with a String: textilizable(text, options)
508 508 # * with an object and one of its attribute: textilizable(issue, :description, options)
509 509 def textilizable(*args)
510 510 options = args.last.is_a?(Hash) ? args.pop : {}
511 511 case args.size
512 512 when 1
513 513 obj = options[:object]
514 514 text = args.shift
515 515 when 2
516 516 obj = args.shift
517 517 attr = args.shift
518 518 text = obj.send(attr).to_s
519 519 else
520 520 raise ArgumentError, 'invalid arguments to textilizable'
521 521 end
522 522 return '' if text.blank?
523 523 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
524 524 only_path = options.delete(:only_path) == false ? false : true
525 525
526 526 text = text.dup
527 527 macros = catch_macros(text)
528 528 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
529 529
530 530 @parsed_headings = []
531 531 @heading_anchors = {}
532 532 @current_section = 0 if options[:edit_section_links]
533 533
534 534 parse_sections(text, project, obj, attr, only_path, options)
535 535 text = parse_non_pre_blocks(text, obj, macros) do |text|
536 536 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
537 537 send method_name, text, project, obj, attr, only_path, options
538 538 end
539 539 end
540 540 parse_headings(text, project, obj, attr, only_path, options)
541 541
542 542 if @parsed_headings.any?
543 543 replace_toc(text, @parsed_headings)
544 544 end
545 545
546 546 text.html_safe
547 547 end
548 548
549 549 def parse_non_pre_blocks(text, obj, macros)
550 550 s = StringScanner.new(text)
551 551 tags = []
552 552 parsed = ''
553 553 while !s.eos?
554 554 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
555 555 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
556 556 if tags.empty?
557 557 yield text
558 558 inject_macros(text, obj, macros) if macros.any?
559 559 else
560 560 inject_macros(text, obj, macros, false) if macros.any?
561 561 end
562 562 parsed << text
563 563 if tag
564 564 if closing
565 565 if tags.last == tag.downcase
566 566 tags.pop
567 567 end
568 568 else
569 569 tags << tag.downcase
570 570 end
571 571 parsed << full_tag
572 572 end
573 573 end
574 574 # Close any non closing tags
575 575 while tag = tags.pop
576 576 parsed << "</#{tag}>"
577 577 end
578 578 parsed
579 579 end
580 580
581 581 def parse_inline_attachments(text, project, obj, attr, only_path, options)
582 582 # when using an image link, try to use an attachment, if possible
583 583 attachments = options[:attachments] || []
584 584 attachments += obj.attachments if obj.respond_to?(:attachments)
585 585 if attachments.present?
586 586 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
587 587 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
588 588 # search for the picture in attachments
589 589 if found = Attachment.latest_attach(attachments, filename)
590 590 image_url = download_named_attachment_path(found, found.filename, :only_path => only_path)
591 591 desc = found.description.to_s.gsub('"', '')
592 592 if !desc.blank? && alttext.blank?
593 593 alt = " title=\"#{desc}\" alt=\"#{desc}\""
594 594 end
595 595 "src=\"#{image_url}\"#{alt}"
596 596 else
597 597 m
598 598 end
599 599 end
600 600 end
601 601 end
602 602
603 603 # Wiki links
604 604 #
605 605 # Examples:
606 606 # [[mypage]]
607 607 # [[mypage|mytext]]
608 608 # wiki links can refer other project wikis, using project name or identifier:
609 609 # [[project:]] -> wiki starting page
610 610 # [[project:|mytext]]
611 611 # [[project:mypage]]
612 612 # [[project:mypage|mytext]]
613 613 def parse_wiki_links(text, project, obj, attr, only_path, options)
614 614 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
615 615 link_project = project
616 616 esc, all, page, title = $1, $2, $3, $5
617 617 if esc.nil?
618 618 if page =~ /^([^\:]+)\:(.*)$/
619 619 identifier, page = $1, $2
620 620 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
621 621 title ||= identifier if page.blank?
622 622 end
623 623
624 624 if link_project && link_project.wiki
625 625 # extract anchor
626 626 anchor = nil
627 627 if page =~ /^(.+?)\#(.+)$/
628 628 page, anchor = $1, $2
629 629 end
630 630 anchor = sanitize_anchor_name(anchor) if anchor.present?
631 631 # check if page exists
632 632 wiki_page = link_project.wiki.find_page(page)
633 633 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
634 634 "##{anchor}"
635 635 else
636 636 case options[:wiki_links]
637 637 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
638 638 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
639 639 else
640 640 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
641 641 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
642 642 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
643 643 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
644 644 end
645 645 end
646 646 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
647 647 else
648 648 # project or wiki doesn't exist
649 649 all
650 650 end
651 651 else
652 652 all
653 653 end
654 654 end
655 655 end
656 656
657 657 # Redmine links
658 658 #
659 659 # Examples:
660 660 # Issues:
661 661 # #52 -> Link to issue #52
662 662 # Changesets:
663 663 # r52 -> Link to revision 52
664 664 # commit:a85130f -> Link to scmid starting with a85130f
665 665 # Documents:
666 666 # document#17 -> Link to document with id 17
667 667 # document:Greetings -> Link to the document with title "Greetings"
668 668 # document:"Some document" -> Link to the document with title "Some document"
669 669 # Versions:
670 670 # version#3 -> Link to version with id 3
671 671 # version:1.0.0 -> Link to version named "1.0.0"
672 672 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
673 673 # Attachments:
674 674 # attachment:file.zip -> Link to the attachment of the current object named file.zip
675 675 # Source files:
676 676 # source:some/file -> Link to the file located at /some/file in the project's repository
677 677 # source:some/file@52 -> Link to the file's revision 52
678 678 # source:some/file#L120 -> Link to line 120 of the file
679 679 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
680 680 # export:some/file -> Force the download of the file
681 681 # Forum messages:
682 682 # message#1218 -> Link to message with id 1218
683 683 # Projects:
684 684 # project:someproject -> Link to project named "someproject"
685 685 # project#3 -> Link to project with id 3
686 686 #
687 687 # Links can refer other objects from other projects, using project identifier:
688 688 # identifier:r52
689 689 # identifier:document:"Some document"
690 690 # identifier:version:1.0.0
691 691 # identifier:source:some/file
692 692 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
693 693 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|
694 694 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
695 695 link = nil
696 696 project = default_project
697 697 if project_identifier
698 698 project = Project.visible.find_by_identifier(project_identifier)
699 699 end
700 700 if esc.nil?
701 701 if prefix.nil? && sep == 'r'
702 702 if project
703 703 repository = nil
704 704 if repo_identifier
705 705 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
706 706 else
707 707 repository = project.repository
708 708 end
709 709 # project.changesets.visible raises an SQL error because of a double join on repositories
710 710 if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier))
711 711 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.revision},
712 712 :class => 'changeset',
713 713 :title => truncate_single_line(changeset.comments, :length => 100))
714 714 end
715 715 end
716 716 elsif sep == '#'
717 717 oid = identifier.to_i
718 718 case prefix
719 719 when nil
720 720 if oid.to_s == identifier && issue = Issue.visible.find_by_id(oid, :include => :status)
721 721 anchor = comment_id ? "note-#{comment_id}" : nil
722 722 link = link_to(h("##{oid}#{comment_suffix}"), {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor},
723 723 :class => issue.css_classes,
724 724 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
725 725 end
726 726 when 'document'
727 727 if document = Document.visible.find_by_id(oid)
728 728 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
729 729 :class => 'document'
730 730 end
731 731 when 'version'
732 732 if version = Version.visible.find_by_id(oid)
733 733 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
734 734 :class => 'version'
735 735 end
736 736 when 'message'
737 737 if message = Message.visible.find_by_id(oid, :include => :parent)
738 738 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
739 739 end
740 740 when 'forum'
741 741 if board = Board.visible.find_by_id(oid)
742 742 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
743 743 :class => 'board'
744 744 end
745 745 when 'news'
746 746 if news = News.visible.find_by_id(oid)
747 747 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
748 748 :class => 'news'
749 749 end
750 750 when 'project'
751 751 if p = Project.visible.find_by_id(oid)
752 752 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
753 753 end
754 754 end
755 755 elsif sep == ':'
756 756 # removes the double quotes if any
757 757 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
758 758 case prefix
759 759 when 'document'
760 760 if project && document = project.documents.visible.find_by_title(name)
761 761 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
762 762 :class => 'document'
763 763 end
764 764 when 'version'
765 765 if project && version = project.versions.visible.find_by_name(name)
766 766 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
767 767 :class => 'version'
768 768 end
769 769 when 'forum'
770 770 if project && board = project.boards.visible.find_by_name(name)
771 771 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
772 772 :class => 'board'
773 773 end
774 774 when 'news'
775 775 if project && news = project.news.visible.find_by_title(name)
776 776 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
777 777 :class => 'news'
778 778 end
779 779 when 'commit', 'source', 'export'
780 780 if project
781 781 repository = nil
782 782 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
783 783 repo_prefix, repo_identifier, name = $1, $2, $3
784 784 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
785 785 else
786 786 repository = project.repository
787 787 end
788 788 if prefix == 'commit'
789 789 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
790 790 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},
791 791 :class => 'changeset',
792 792 :title => truncate_single_line(changeset.comments, :length => 100)
793 793 end
794 794 else
795 795 if repository && User.current.allowed_to?(:browse_repository, project)
796 796 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
797 797 path, rev, anchor = $1, $3, $5
798 798 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
799 799 :path => to_path_param(path),
800 800 :rev => rev,
801 801 :anchor => anchor},
802 802 :class => (prefix == 'export' ? 'source download' : 'source')
803 803 end
804 804 end
805 805 repo_prefix = nil
806 806 end
807 807 when 'attachment'
808 808 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
809 809 if attachments && attachment = Attachment.latest_attach(attachments, name)
810 810 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
811 811 end
812 812 when 'project'
813 813 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
814 814 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
815 815 end
816 816 end
817 817 end
818 818 end
819 819 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
820 820 end
821 821 end
822 822
823 823 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
824 824
825 825 def parse_sections(text, project, obj, attr, only_path, options)
826 826 return unless options[:edit_section_links]
827 827 text.gsub!(HEADING_RE) do
828 828 heading = $1
829 829 @current_section += 1
830 830 if @current_section > 1
831 831 content_tag('div',
832 832 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
833 833 :class => 'contextual',
834 834 :title => l(:button_edit_section),
835 835 :id => "section-#{@current_section}") + heading.html_safe
836 836 else
837 837 heading
838 838 end
839 839 end
840 840 end
841 841
842 842 # Headings and TOC
843 843 # Adds ids and links to headings unless options[:headings] is set to false
844 844 def parse_headings(text, project, obj, attr, only_path, options)
845 845 return if options[:headings] == false
846 846
847 847 text.gsub!(HEADING_RE) do
848 848 level, attrs, content = $2.to_i, $3, $4
849 849 item = strip_tags(content).strip
850 850 anchor = sanitize_anchor_name(item)
851 851 # used for single-file wiki export
852 852 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
853 853 @heading_anchors[anchor] ||= 0
854 854 idx = (@heading_anchors[anchor] += 1)
855 855 if idx > 1
856 856 anchor = "#{anchor}-#{idx}"
857 857 end
858 858 @parsed_headings << [level, anchor, item]
859 859 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
860 860 end
861 861 end
862 862
863 863 MACROS_RE = /(
864 864 (!)? # escaping
865 865 (
866 866 \{\{ # opening tag
867 867 ([\w]+) # macro name
868 868 (\(([^\n\r]*?)\))? # optional arguments
869 869 ([\n\r].*?[\n\r])? # optional block of text
870 870 \}\} # closing tag
871 871 )
872 872 )/mx unless const_defined?(:MACROS_RE)
873 873
874 874 MACRO_SUB_RE = /(
875 875 \{\{
876 876 macro\((\d+)\)
877 877 \}\}
878 878 )/x unless const_defined?(:MACRO_SUB_RE)
879 879
880 880 # Extracts macros from text
881 881 def catch_macros(text)
882 882 macros = {}
883 883 text.gsub!(MACROS_RE) do
884 884 all, macro = $1, $4.downcase
885 885 if macro_exists?(macro) || all =~ MACRO_SUB_RE
886 886 index = macros.size
887 887 macros[index] = all
888 888 "{{macro(#{index})}}"
889 889 else
890 890 all
891 891 end
892 892 end
893 893 macros
894 894 end
895 895
896 896 # Executes and replaces macros in text
897 897 def inject_macros(text, obj, macros, execute=true)
898 898 text.gsub!(MACRO_SUB_RE) do
899 899 all, index = $1, $2.to_i
900 900 orig = macros.delete(index)
901 901 if execute && orig && orig =~ MACROS_RE
902 902 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
903 903 if esc.nil?
904 904 h(exec_macro(macro, obj, args, block) || all)
905 905 else
906 906 h(all)
907 907 end
908 908 elsif orig
909 909 h(orig)
910 910 else
911 911 h(all)
912 912 end
913 913 end
914 914 end
915 915
916 916 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
917 917
918 918 # Renders the TOC with given headings
919 919 def replace_toc(text, headings)
920 920 text.gsub!(TOC_RE) do
921 921 # Keep only the 4 first levels
922 922 headings = headings.select{|level, anchor, item| level <= 4}
923 923 if headings.empty?
924 924 ''
925 925 else
926 926 div_class = 'toc'
927 927 div_class << ' right' if $1 == '>'
928 928 div_class << ' left' if $1 == '<'
929 929 out = "<ul class=\"#{div_class}\"><li>"
930 930 root = headings.map(&:first).min
931 931 current = root
932 932 started = false
933 933 headings.each do |level, anchor, item|
934 934 if level > current
935 935 out << '<ul><li>' * (level - current)
936 936 elsif level < current
937 937 out << "</li></ul>\n" * (current - level) + "</li><li>"
938 938 elsif started
939 939 out << '</li><li>'
940 940 end
941 941 out << "<a href=\"##{anchor}\">#{item}</a>"
942 942 current = level
943 943 started = true
944 944 end
945 945 out << '</li></ul>' * (current - root)
946 946 out << '</li></ul>'
947 947 end
948 948 end
949 949 end
950 950
951 951 # Same as Rails' simple_format helper without using paragraphs
952 952 def simple_format_without_paragraph(text)
953 953 text.to_s.
954 954 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
955 955 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
956 956 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
957 957 html_safe
958 958 end
959 959
960 960 def lang_options_for_select(blank=true)
961 961 (blank ? [["(auto)", ""]] : []) + languages_options
962 962 end
963 963
964 964 def label_tag_for(name, option_tags = nil, options = {})
965 965 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
966 966 content_tag("label", label_text)
967 967 end
968 968
969 969 def labelled_form_for(*args, &proc)
970 970 args << {} unless args.last.is_a?(Hash)
971 971 options = args.last
972 972 if args.first.is_a?(Symbol)
973 973 options.merge!(:as => args.shift)
974 974 end
975 975 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
976 976 form_for(*args, &proc)
977 977 end
978 978
979 979 def labelled_fields_for(*args, &proc)
980 980 args << {} unless args.last.is_a?(Hash)
981 981 options = args.last
982 982 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
983 983 fields_for(*args, &proc)
984 984 end
985 985
986 986 def labelled_remote_form_for(*args, &proc)
987 987 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_remote_form_for is deprecated and will be removed in Redmine 2.2."
988 988 args << {} unless args.last.is_a?(Hash)
989 989 options = args.last
990 990 options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
991 991 form_for(*args, &proc)
992 992 end
993 993
994 994 def error_messages_for(*objects)
995 995 html = ""
996 996 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
997 997 errors = objects.map {|o| o.errors.full_messages}.flatten
998 998 if errors.any?
999 999 html << "<div id='errorExplanation'><ul>\n"
1000 1000 errors.each do |error|
1001 1001 html << "<li>#{h error}</li>\n"
1002 1002 end
1003 1003 html << "</ul></div>\n"
1004 1004 end
1005 1005 html.html_safe
1006 1006 end
1007 1007
1008 1008 def delete_link(url, options={})
1009 1009 options = {
1010 1010 :method => :delete,
1011 1011 :data => {:confirm => l(:text_are_you_sure)},
1012 1012 :class => 'icon icon-del'
1013 1013 }.merge(options)
1014 1014
1015 1015 link_to l(:button_delete), url, options
1016 1016 end
1017 1017
1018 1018 def preview_link(url, form, target='preview', options={})
1019 1019 content_tag 'a', l(:label_preview), {
1020 1020 :href => "#",
1021 1021 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1022 1022 :accesskey => accesskey(:preview)
1023 1023 }.merge(options)
1024 1024 end
1025 1025
1026 1026 def link_to_function(name, function, html_options={})
1027 1027 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1028 1028 end
1029 1029
1030 1030 # Helper to render JSON in views
1031 1031 def raw_json(arg)
1032 1032 arg.to_json.to_s.gsub('/', '\/').html_safe
1033 1033 end
1034 1034
1035 1035 def back_url
1036 1036 url = params[:back_url]
1037 1037 if url.nil? && referer = request.env['HTTP_REFERER']
1038 1038 url = CGI.unescape(referer.to_s)
1039 1039 end
1040 1040 url
1041 1041 end
1042 1042
1043 1043 def back_url_hidden_field_tag
1044 1044 url = back_url
1045 1045 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1046 1046 end
1047 1047
1048 1048 def check_all_links(form_name)
1049 1049 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1050 1050 " | ".html_safe +
1051 1051 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1052 1052 end
1053 1053
1054 1054 def progress_bar(pcts, options={})
1055 1055 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1056 1056 pcts = pcts.collect(&:round)
1057 1057 pcts[1] = pcts[1] - pcts[0]
1058 1058 pcts << (100 - pcts[1] - pcts[0])
1059 1059 width = options[:width] || '100px;'
1060 1060 legend = options[:legend] || ''
1061 1061 content_tag('table',
1062 1062 content_tag('tr',
1063 1063 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1064 1064 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1065 1065 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1066 1066 ), :class => 'progress progress-#{pcts[0]}', :style => "width: #{width};").html_safe +
1067 1067 content_tag('p', legend, :class => 'percent').html_safe
1068 1068 end
1069 1069
1070 1070 def checked_image(checked=true)
1071 1071 if checked
1072 1072 image_tag 'toggle_check.png'
1073 1073 end
1074 1074 end
1075 1075
1076 1076 def context_menu(url)
1077 1077 unless @context_menu_included
1078 1078 content_for :header_tags do
1079 1079 javascript_include_tag('context_menu') +
1080 1080 stylesheet_link_tag('context_menu')
1081 1081 end
1082 1082 if l(:direction) == 'rtl'
1083 1083 content_for :header_tags do
1084 1084 stylesheet_link_tag('context_menu_rtl')
1085 1085 end
1086 1086 end
1087 1087 @context_menu_included = true
1088 1088 end
1089 1089 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1090 1090 end
1091 1091
1092 1092 def calendar_for(field_id)
1093 1093 include_calendar_headers_tags
1094 1094 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1095 1095 end
1096 1096
1097 1097 def include_calendar_headers_tags
1098 1098 unless @calendar_headers_tags_included
1099 1099 tags = javascript_include_tag("datepicker")
1100 1100 @calendar_headers_tags_included = true
1101 1101 content_for :header_tags do
1102 1102 start_of_week = Setting.start_of_week
1103 1103 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1104 1104 # Redmine uses 1..7 (monday..sunday) in settings and locales
1105 1105 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1106 1106 start_of_week = start_of_week.to_i % 7
1107 1107 tags << javascript_tag(
1108 1108 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1109 1109 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1110 1110 path_to_image('/images/calendar.png') +
1111 1111 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1112 1112 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1113 1113 "beforeShow: beforeShowDatePicker};")
1114 1114 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1115 1115 unless jquery_locale == 'en'
1116 1116 tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js")
1117 1117 end
1118 1118 tags
1119 1119 end
1120 1120 end
1121 1121 end
1122 1122
1123 1123 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1124 1124 # Examples:
1125 1125 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1126 1126 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1127 1127 #
1128 1128 def stylesheet_link_tag(*sources)
1129 1129 options = sources.last.is_a?(Hash) ? sources.pop : {}
1130 1130 plugin = options.delete(:plugin)
1131 1131 sources = sources.map do |source|
1132 1132 if plugin
1133 1133 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1134 1134 elsif current_theme && current_theme.stylesheets.include?(source)
1135 1135 current_theme.stylesheet_path(source)
1136 1136 else
1137 1137 source
1138 1138 end
1139 1139 end
1140 1140 super sources, options
1141 1141 end
1142 1142
1143 1143 # Overrides Rails' image_tag with themes and plugins support.
1144 1144 # Examples:
1145 1145 # image_tag('image.png') # => picks image.png from the current theme or defaults
1146 1146 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1147 1147 #
1148 1148 def image_tag(source, options={})
1149 1149 if plugin = options.delete(:plugin)
1150 1150 source = "/plugin_assets/#{plugin}/images/#{source}"
1151 1151 elsif current_theme && current_theme.images.include?(source)
1152 1152 source = current_theme.image_path(source)
1153 1153 end
1154 1154 super source, options
1155 1155 end
1156 1156
1157 1157 # Overrides Rails' javascript_include_tag with plugins support
1158 1158 # Examples:
1159 1159 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1160 1160 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1161 1161 #
1162 1162 def javascript_include_tag(*sources)
1163 1163 options = sources.last.is_a?(Hash) ? sources.pop : {}
1164 1164 if plugin = options.delete(:plugin)
1165 1165 sources = sources.map do |source|
1166 1166 if plugin
1167 1167 "/plugin_assets/#{plugin}/javascripts/#{source}"
1168 1168 else
1169 1169 source
1170 1170 end
1171 1171 end
1172 1172 end
1173 1173 super sources, options
1174 1174 end
1175 1175
1176 def content_for(name, content = nil, &block)
1177 @has_content ||= {}
1178 @has_content[name] = true
1179 super(name, content, &block)
1180 end
1181
1176 # TODO: remove this in 2.5.0
1182 1177 def has_content?(name)
1183 (@has_content && @has_content[name]) || false
1178 content_for?(name)
1184 1179 end
1185 1180
1186 1181 def sidebar_content?
1187 has_content?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1182 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1188 1183 end
1189 1184
1190 1185 def view_layouts_base_sidebar_hook_response
1191 1186 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1192 1187 end
1193 1188
1194 1189 def email_delivery_enabled?
1195 1190 !!ActionMailer::Base.perform_deliveries
1196 1191 end
1197 1192
1198 1193 # Returns the avatar image tag for the given +user+ if avatars are enabled
1199 1194 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1200 1195 def avatar(user, options = { })
1201 1196 if Setting.gravatar_enabled?
1202 1197 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1203 1198 email = nil
1204 1199 if user.respond_to?(:mail)
1205 1200 email = user.mail
1206 1201 elsif user.to_s =~ %r{<(.+?)>}
1207 1202 email = $1
1208 1203 end
1209 1204 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1210 1205 else
1211 1206 ''
1212 1207 end
1213 1208 end
1214 1209
1215 1210 def sanitize_anchor_name(anchor)
1216 1211 if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java'
1217 1212 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1218 1213 else
1219 1214 # TODO: remove when ruby1.8 is no longer supported
1220 1215 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1221 1216 end
1222 1217 end
1223 1218
1224 1219 # Returns the javascript tags that are included in the html layout head
1225 1220 def javascript_heads
1226 1221 tags = javascript_include_tag('jquery-1.8.3-ui-1.9.2-ujs-2.0.3', 'application')
1227 1222 unless User.current.pref.warn_on_leaving_unsaved == '0'
1228 1223 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1229 1224 end
1230 1225 tags
1231 1226 end
1232 1227
1233 1228 def favicon
1234 1229 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1235 1230 end
1236 1231
1237 1232 def robot_exclusion_tag
1238 1233 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1239 1234 end
1240 1235
1241 1236 # Returns true if arg is expected in the API response
1242 1237 def include_in_api_response?(arg)
1243 1238 unless @included_in_api_response
1244 1239 param = params[:include]
1245 1240 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1246 1241 @included_in_api_response.collect!(&:strip)
1247 1242 end
1248 1243 @included_in_api_response.include?(arg.to_s)
1249 1244 end
1250 1245
1251 1246 # Returns options or nil if nometa param or X-Redmine-Nometa header
1252 1247 # was set in the request
1253 1248 def api_meta(options)
1254 1249 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1255 1250 # compatibility mode for activeresource clients that raise
1256 1251 # an error when unserializing an array with attributes
1257 1252 nil
1258 1253 else
1259 1254 options
1260 1255 end
1261 1256 end
1262 1257
1263 1258 private
1264 1259
1265 1260 def wiki_helper
1266 1261 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1267 1262 extend helper
1268 1263 return self
1269 1264 end
1270 1265
1271 1266 def link_to_content_update(text, url_params = {}, html_options = {})
1272 1267 link_to(text, url_params, html_options)
1273 1268 end
1274 1269 end
@@ -1,597 +1,607
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2013 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 ProjectsControllerTest < ActionController::TestCase
21 21 fixtures :projects, :versions, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details,
22 22 :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages,
23 23 :attachments, :custom_fields, :custom_values, :time_entries
24 24
25 25 def setup
26 26 @request.session[:user_id] = nil
27 27 Setting.default_language = 'en'
28 28 end
29 29
30 30 def test_index_by_anonymous_should_not_show_private_projects
31 31 get :index
32 32 assert_response :success
33 33 assert_template 'index'
34 34 projects = assigns(:projects)
35 35 assert_not_nil projects
36 36 assert projects.all?(&:is_public?)
37 37
38 38 assert_select 'ul' do
39 39 assert_select 'li' do
40 40 assert_select 'a', :text => 'eCookbook'
41 41 assert_select 'ul' do
42 42 assert_select 'a', :text => 'Child of private child'
43 43 end
44 44 end
45 45 end
46 46 assert_select 'a', :text => /Private child of eCookbook/, :count => 0
47 47 end
48 48
49 49 def test_index_atom
50 50 get :index, :format => 'atom'
51 51 assert_response :success
52 52 assert_template 'common/feed'
53 53 assert_select 'feed>title', :text => 'Redmine: Latest projects'
54 54 assert_select 'feed>entry', :count => Project.visible(User.current).count
55 55 end
56 56
57 57 test "#index by non-admin user with view_time_entries permission should show overall spent time link" do
58 58 @request.session[:user_id] = 3
59 59 get :index
60 60 assert_template 'index'
61 61 assert_select 'a[href=?]', '/time_entries'
62 62 end
63 63
64 64 test "#index by non-admin user without view_time_entries permission should not show overall spent time link" do
65 65 Role.find(2).remove_permission! :view_time_entries
66 66 Role.non_member.remove_permission! :view_time_entries
67 67 Role.anonymous.remove_permission! :view_time_entries
68 68 @request.session[:user_id] = 3
69 69
70 70 get :index
71 71 assert_template 'index'
72 72 assert_select 'a[href=?]', '/time_entries', 0
73 73 end
74 74
75 75 test "#new by admin user should accept get" do
76 76 @request.session[:user_id] = 1
77 77
78 78 get :new
79 79 assert_response :success
80 80 assert_template 'new'
81 81 end
82 82
83 83 test "#new by non-admin user with add_project permission should accept get" do
84 84 Role.non_member.add_permission! :add_project
85 85 @request.session[:user_id] = 9
86 86
87 87 get :new
88 88 assert_response :success
89 89 assert_template 'new'
90 90 assert_select 'select[name=?]', 'project[parent_id]', 0
91 91 end
92 92
93 93 test "#new by non-admin user with add_subprojects permission should accept get" do
94 94 Role.find(1).remove_permission! :add_project
95 95 Role.find(1).add_permission! :add_subprojects
96 96 @request.session[:user_id] = 2
97 97
98 98 get :new, :parent_id => 'ecookbook'
99 99 assert_response :success
100 100 assert_template 'new'
101 101
102 102 assert_select 'select[name=?]', 'project[parent_id]' do
103 103 # parent project selected
104 104 assert_select 'option[value=1][selected=selected]'
105 105 # no empty value
106 106 assert_select 'option[value=]', 0
107 107 end
108 108 end
109 109
110 110 test "#create by admin user should create a new project" do
111 111 @request.session[:user_id] = 1
112 112
113 113 post :create,
114 114 :project => {
115 115 :name => "blog",
116 116 :description => "weblog",
117 117 :homepage => 'http://weblog',
118 118 :identifier => "blog",
119 119 :is_public => 1,
120 120 :custom_field_values => { '3' => 'Beta' },
121 121 :tracker_ids => ['1', '3'],
122 122 # an issue custom field that is not for all project
123 123 :issue_custom_field_ids => ['9'],
124 124 :enabled_module_names => ['issue_tracking', 'news', 'repository']
125 125 }
126 126 assert_redirected_to '/projects/blog/settings'
127 127
128 128 project = Project.find_by_name('blog')
129 129 assert_kind_of Project, project
130 130 assert project.active?
131 131 assert_equal 'weblog', project.description
132 132 assert_equal 'http://weblog', project.homepage
133 133 assert_equal true, project.is_public?
134 134 assert_nil project.parent
135 135 assert_equal 'Beta', project.custom_value_for(3).value
136 136 assert_equal [1, 3], project.trackers.map(&:id).sort
137 137 assert_equal ['issue_tracking', 'news', 'repository'], project.enabled_module_names.sort
138 138 assert project.issue_custom_fields.include?(IssueCustomField.find(9))
139 139 end
140 140
141 141 test "#create by admin user should create a new subproject" do
142 142 @request.session[:user_id] = 1
143 143
144 144 assert_difference 'Project.count' do
145 145 post :create, :project => { :name => "blog",
146 146 :description => "weblog",
147 147 :identifier => "blog",
148 148 :is_public => 1,
149 149 :custom_field_values => { '3' => 'Beta' },
150 150 :parent_id => 1
151 151 }
152 152 assert_redirected_to '/projects/blog/settings'
153 153 end
154 154
155 155 project = Project.find_by_name('blog')
156 156 assert_kind_of Project, project
157 157 assert_equal Project.find(1), project.parent
158 158 end
159 159
160 160 test "#create by admin user should continue" do
161 161 @request.session[:user_id] = 1
162 162
163 163 assert_difference 'Project.count' do
164 164 post :create, :project => {:name => "blog", :identifier => "blog"}, :continue => 'Create and continue'
165 165 end
166 166 assert_redirected_to '/projects/new'
167 167 end
168 168
169 169 test "#create by non-admin user with add_project permission should create a new project" do
170 170 Role.non_member.add_permission! :add_project
171 171 @request.session[:user_id] = 9
172 172
173 173 post :create, :project => { :name => "blog",
174 174 :description => "weblog",
175 175 :identifier => "blog",
176 176 :is_public => 1,
177 177 :custom_field_values => { '3' => 'Beta' },
178 178 :tracker_ids => ['1', '3'],
179 179 :enabled_module_names => ['issue_tracking', 'news', 'repository']
180 180 }
181 181
182 182 assert_redirected_to '/projects/blog/settings'
183 183
184 184 project = Project.find_by_name('blog')
185 185 assert_kind_of Project, project
186 186 assert_equal 'weblog', project.description
187 187 assert_equal true, project.is_public?
188 188 assert_equal [1, 3], project.trackers.map(&:id).sort
189 189 assert_equal ['issue_tracking', 'news', 'repository'], project.enabled_module_names.sort
190 190
191 191 # User should be added as a project member
192 192 assert User.find(9).member_of?(project)
193 193 assert_equal 1, project.members.size
194 194 end
195 195
196 196 test "#create by non-admin user with add_project permission should fail with parent_id" do
197 197 Role.non_member.add_permission! :add_project
198 198 @request.session[:user_id] = 9
199 199
200 200 assert_no_difference 'Project.count' do
201 201 post :create, :project => { :name => "blog",
202 202 :description => "weblog",
203 203 :identifier => "blog",
204 204 :is_public => 1,
205 205 :custom_field_values => { '3' => 'Beta' },
206 206 :parent_id => 1
207 207 }
208 208 end
209 209 assert_response :success
210 210 project = assigns(:project)
211 211 assert_kind_of Project, project
212 212 assert_not_equal [], project.errors[:parent_id]
213 213 end
214 214
215 215 test "#create by non-admin user with add_subprojects permission should create a project with a parent_id" do
216 216 Role.find(1).remove_permission! :add_project
217 217 Role.find(1).add_permission! :add_subprojects
218 218 @request.session[:user_id] = 2
219 219
220 220 post :create, :project => { :name => "blog",
221 221 :description => "weblog",
222 222 :identifier => "blog",
223 223 :is_public => 1,
224 224 :custom_field_values => { '3' => 'Beta' },
225 225 :parent_id => 1
226 226 }
227 227 assert_redirected_to '/projects/blog/settings'
228 228 project = Project.find_by_name('blog')
229 229 end
230 230
231 231 test "#create by non-admin user with add_subprojects permission should fail without parent_id" do
232 232 Role.find(1).remove_permission! :add_project
233 233 Role.find(1).add_permission! :add_subprojects
234 234 @request.session[:user_id] = 2
235 235
236 236 assert_no_difference 'Project.count' do
237 237 post :create, :project => { :name => "blog",
238 238 :description => "weblog",
239 239 :identifier => "blog",
240 240 :is_public => 1,
241 241 :custom_field_values => { '3' => 'Beta' }
242 242 }
243 243 end
244 244 assert_response :success
245 245 project = assigns(:project)
246 246 assert_kind_of Project, project
247 247 assert_not_equal [], project.errors[:parent_id]
248 248 end
249 249
250 250 test "#create by non-admin user with add_subprojects permission should fail with unauthorized parent_id" do
251 251 Role.find(1).remove_permission! :add_project
252 252 Role.find(1).add_permission! :add_subprojects
253 253 @request.session[:user_id] = 2
254 254
255 255 assert !User.find(2).member_of?(Project.find(6))
256 256 assert_no_difference 'Project.count' do
257 257 post :create, :project => { :name => "blog",
258 258 :description => "weblog",
259 259 :identifier => "blog",
260 260 :is_public => 1,
261 261 :custom_field_values => { '3' => 'Beta' },
262 262 :parent_id => 6
263 263 }
264 264 end
265 265 assert_response :success
266 266 project = assigns(:project)
267 267 assert_kind_of Project, project
268 268 assert_not_equal [], project.errors[:parent_id]
269 269 end
270 270
271 271 def test_create_subproject_with_inherit_members_should_inherit_members
272 272 Role.find_by_name('Manager').add_permission! :add_subprojects
273 273 parent = Project.find(1)
274 274 @request.session[:user_id] = 2
275 275
276 276 assert_difference 'Project.count' do
277 277 post :create, :project => {
278 278 :name => 'inherited', :identifier => 'inherited', :parent_id => parent.id, :inherit_members => '1'
279 279 }
280 280 assert_response 302
281 281 end
282 282
283 283 project = Project.order('id desc').first
284 284 assert_equal 'inherited', project.name
285 285 assert_equal parent, project.parent
286 286 assert project.memberships.count > 0
287 287 assert_equal parent.memberships.count, project.memberships.count
288 288 end
289 289
290 290 def test_create_should_preserve_modules_on_validation_failure
291 291 with_settings :default_projects_modules => ['issue_tracking', 'repository'] do
292 292 @request.session[:user_id] = 1
293 293 assert_no_difference 'Project.count' do
294 294 post :create, :project => {
295 295 :name => "blog",
296 296 :identifier => "",
297 297 :enabled_module_names => %w(issue_tracking news)
298 298 }
299 299 end
300 300 assert_response :success
301 301 project = assigns(:project)
302 302 assert_equal %w(issue_tracking news), project.enabled_module_names.sort
303 303 end
304 304 end
305 305
306 306 def test_show_by_id
307 307 get :show, :id => 1
308 308 assert_response :success
309 309 assert_template 'show'
310 310 assert_not_nil assigns(:project)
311 311 end
312 312
313 313 def test_show_by_identifier
314 314 get :show, :id => 'ecookbook'
315 315 assert_response :success
316 316 assert_template 'show'
317 317 assert_not_nil assigns(:project)
318 318 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
319 319
320 320 assert_select 'li', :text => /Development status/
321 321 end
322 322
323 def test_show_should_not_display_empty_sidebar
324 p = Project.find(1)
325 p.enabled_module_names = []
326 p.save!
327
328 get :show, :id => 'ecookbook'
329 assert_response :success
330 assert_select '#main.nosidebar'
331 end
332
323 333 def test_show_should_not_display_hidden_custom_fields
324 334 ProjectCustomField.find_by_name('Development status').update_attribute :visible, false
325 335 get :show, :id => 'ecookbook'
326 336 assert_response :success
327 337 assert_template 'show'
328 338 assert_not_nil assigns(:project)
329 339
330 340 assert_select 'li', :text => /Development status/, :count => 0
331 341 end
332 342
333 343 def test_show_should_not_fail_when_custom_values_are_nil
334 344 project = Project.find_by_identifier('ecookbook')
335 345 project.custom_values.first.update_attribute(:value, nil)
336 346 get :show, :id => 'ecookbook'
337 347 assert_response :success
338 348 assert_template 'show'
339 349 assert_not_nil assigns(:project)
340 350 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
341 351 end
342 352
343 353 def show_archived_project_should_be_denied
344 354 project = Project.find_by_identifier('ecookbook')
345 355 project.archive!
346 356
347 357 get :show, :id => 'ecookbook'
348 358 assert_response 403
349 359 assert_nil assigns(:project)
350 360 assert_select 'p', :text => /archived/
351 361 end
352 362
353 363 def test_show_should_not_show_private_subprojects_that_are_not_visible
354 364 get :show, :id => 'ecookbook'
355 365 assert_response :success
356 366 assert_template 'show'
357 367 assert_select 'a', :text => /Private child/, :count => 0
358 368 end
359 369
360 370 def test_show_should_show_private_subprojects_that_are_visible
361 371 @request.session[:user_id] = 2 # manager who is a member of the private subproject
362 372 get :show, :id => 'ecookbook'
363 373 assert_response :success
364 374 assert_template 'show'
365 375 assert_select 'a', :text => /Private child/
366 376 end
367 377
368 378 def test_settings
369 379 @request.session[:user_id] = 2 # manager
370 380 get :settings, :id => 1
371 381 assert_response :success
372 382 assert_template 'settings'
373 383 end
374 384
375 385 def test_settings_of_subproject
376 386 @request.session[:user_id] = 2
377 387 get :settings, :id => 'private-child'
378 388 assert_response :success
379 389 assert_template 'settings'
380 390
381 391 assert_select 'input[type=checkbox][name=?]', 'project[inherit_members]'
382 392 end
383 393
384 394 def test_settings_should_be_denied_for_member_on_closed_project
385 395 Project.find(1).close
386 396 @request.session[:user_id] = 2 # manager
387 397
388 398 get :settings, :id => 1
389 399 assert_response 403
390 400 end
391 401
392 402 def test_settings_should_be_denied_for_anonymous_on_closed_project
393 403 Project.find(1).close
394 404
395 405 get :settings, :id => 1
396 406 assert_response 302
397 407 end
398 408
399 409 def test_update
400 410 @request.session[:user_id] = 2 # manager
401 411 post :update, :id => 1, :project => {:name => 'Test changed name',
402 412 :issue_custom_field_ids => ['']}
403 413 assert_redirected_to '/projects/ecookbook/settings'
404 414 project = Project.find(1)
405 415 assert_equal 'Test changed name', project.name
406 416 end
407 417
408 418 def test_update_with_failure
409 419 @request.session[:user_id] = 2 # manager
410 420 post :update, :id => 1, :project => {:name => ''}
411 421 assert_response :success
412 422 assert_template 'settings'
413 423 assert_error_tag :content => /name can&#x27;t be blank/i
414 424 end
415 425
416 426 def test_update_should_be_denied_for_member_on_closed_project
417 427 Project.find(1).close
418 428 @request.session[:user_id] = 2 # manager
419 429
420 430 post :update, :id => 1, :project => {:name => 'Closed'}
421 431 assert_response 403
422 432 assert_equal 'eCookbook', Project.find(1).name
423 433 end
424 434
425 435 def test_update_should_be_denied_for_anonymous_on_closed_project
426 436 Project.find(1).close
427 437
428 438 post :update, :id => 1, :project => {:name => 'Closed'}
429 439 assert_response 302
430 440 assert_equal 'eCookbook', Project.find(1).name
431 441 end
432 442
433 443 def test_modules
434 444 @request.session[:user_id] = 2
435 445 Project.find(1).enabled_module_names = ['issue_tracking', 'news']
436 446
437 447 post :modules, :id => 1, :enabled_module_names => ['issue_tracking', 'repository', 'documents']
438 448 assert_redirected_to '/projects/ecookbook/settings/modules'
439 449 assert_equal ['documents', 'issue_tracking', 'repository'], Project.find(1).enabled_module_names.sort
440 450 end
441 451
442 452 def test_destroy_leaf_project_without_confirmation_should_show_confirmation
443 453 @request.session[:user_id] = 1 # admin
444 454
445 455 assert_no_difference 'Project.count' do
446 456 delete :destroy, :id => 2
447 457 assert_response :success
448 458 assert_template 'destroy'
449 459 end
450 460 end
451 461
452 462 def test_destroy_without_confirmation_should_show_confirmation_with_subprojects
453 463 @request.session[:user_id] = 1 # admin
454 464
455 465 assert_no_difference 'Project.count' do
456 466 delete :destroy, :id => 1
457 467 assert_response :success
458 468 assert_template 'destroy'
459 469 end
460 470 assert_select 'strong',
461 471 :text => ['Private child of eCookbook',
462 472 'Child of private child, eCookbook Subproject 1',
463 473 'eCookbook Subproject 2'].join(', ')
464 474 end
465 475
466 476 def test_destroy_with_confirmation_should_destroy_the_project_and_subprojects
467 477 @request.session[:user_id] = 1 # admin
468 478
469 479 assert_difference 'Project.count', -5 do
470 480 delete :destroy, :id => 1, :confirm => 1
471 481 assert_redirected_to '/admin/projects'
472 482 end
473 483 assert_nil Project.find_by_id(1)
474 484 end
475 485
476 486 def test_archive
477 487 @request.session[:user_id] = 1 # admin
478 488 post :archive, :id => 1
479 489 assert_redirected_to '/admin/projects'
480 490 assert !Project.find(1).active?
481 491 end
482 492
483 493 def test_archive_with_failure
484 494 @request.session[:user_id] = 1
485 495 Project.any_instance.stubs(:archive).returns(false)
486 496 post :archive, :id => 1
487 497 assert_redirected_to '/admin/projects'
488 498 assert_match /project cannot be archived/i, flash[:error]
489 499 end
490 500
491 501 def test_unarchive
492 502 @request.session[:user_id] = 1 # admin
493 503 Project.find(1).archive
494 504 post :unarchive, :id => 1
495 505 assert_redirected_to '/admin/projects'
496 506 assert Project.find(1).active?
497 507 end
498 508
499 509 def test_close
500 510 @request.session[:user_id] = 2
501 511 post :close, :id => 1
502 512 assert_redirected_to '/projects/ecookbook'
503 513 assert_equal Project::STATUS_CLOSED, Project.find(1).status
504 514 end
505 515
506 516 def test_reopen
507 517 Project.find(1).close
508 518 @request.session[:user_id] = 2
509 519 post :reopen, :id => 1
510 520 assert_redirected_to '/projects/ecookbook'
511 521 assert Project.find(1).active?
512 522 end
513 523
514 524 def test_project_breadcrumbs_should_be_limited_to_3_ancestors
515 525 CustomField.delete_all
516 526 parent = nil
517 527 6.times do |i|
518 528 p = Project.generate_with_parent!(parent)
519 529 get :show, :id => p
520 530 assert_select '#header h1' do
521 531 assert_select 'a', :count => [i, 3].min
522 532 end
523 533
524 534 parent = p
525 535 end
526 536 end
527 537
528 538 def test_get_copy
529 539 @request.session[:user_id] = 1 # admin
530 540 get :copy, :id => 1
531 541 assert_response :success
532 542 assert_template 'copy'
533 543 assert assigns(:project)
534 544 assert_equal Project.find(1).description, assigns(:project).description
535 545 assert_nil assigns(:project).id
536 546
537 547 assert_select 'input[name=?][value=?]', 'project[enabled_module_names][]', 'issue_tracking', 1
538 548 end
539 549
540 550 def test_get_copy_with_invalid_source_should_respond_with_404
541 551 @request.session[:user_id] = 1
542 552 get :copy, :id => 99
543 553 assert_response 404
544 554 end
545 555
546 556 def test_post_copy_should_copy_requested_items
547 557 @request.session[:user_id] = 1 # admin
548 558 CustomField.delete_all
549 559
550 560 assert_difference 'Project.count' do
551 561 post :copy, :id => 1,
552 562 :project => {
553 563 :name => 'Copy',
554 564 :identifier => 'unique-copy',
555 565 :tracker_ids => ['1', '2', '3', ''],
556 566 :enabled_module_names => %w(issue_tracking time_tracking)
557 567 },
558 568 :only => %w(issues versions)
559 569 end
560 570 project = Project.find('unique-copy')
561 571 source = Project.find(1)
562 572 assert_equal %w(issue_tracking time_tracking), project.enabled_module_names.sort
563 573
564 574 assert_equal source.versions.count, project.versions.count, "All versions were not copied"
565 575 assert_equal source.issues.count, project.issues.count, "All issues were not copied"
566 576 assert_equal 0, project.members.count
567 577 end
568 578
569 579 def test_post_copy_should_redirect_to_settings_when_successful
570 580 @request.session[:user_id] = 1 # admin
571 581 post :copy, :id => 1, :project => {:name => 'Copy', :identifier => 'unique-copy'}
572 582 assert_response :redirect
573 583 assert_redirected_to :controller => 'projects', :action => 'settings', :id => 'unique-copy'
574 584 end
575 585
576 586 def test_jump_should_redirect_to_active_tab
577 587 get :show, :id => 1, :jump => 'issues'
578 588 assert_redirected_to '/projects/ecookbook/issues'
579 589 end
580 590
581 591 def test_jump_should_not_redirect_to_inactive_tab
582 592 get :show, :id => 3, :jump => 'documents'
583 593 assert_response :success
584 594 assert_template 'show'
585 595 end
586 596
587 597 def test_jump_should_not_redirect_to_unknown_tab
588 598 get :show, :id => 3, :jump => 'foobar'
589 599 assert_response :success
590 600 assert_template 'show'
591 601 end
592 602
593 603 def test_body_should_have_project_css_class
594 604 get :show, :id => 1
595 605 assert_select 'body.project-ecookbook'
596 606 end
597 607 end
General Comments 0
You need to be logged in to leave comments. Login now