##// END OF EJS Templates
Adds a project specific css class to body (#14767)....
Jean-Philippe Lang -
r11940:481a1311b75e
parent child
Show More
@@ -1,1272 +1,1273
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('acronym', 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 css << 'project-' + @project.identifier if @project && @project.identifier.present?
491 492 css << 'controller-' + controller_name
492 493 css << 'action-' + action_name
493 494 css.join(' ')
494 495 end
495 496
496 497 def accesskey(s)
497 498 @used_accesskeys ||= []
498 499 key = Redmine::AccessKeys.key_for(s)
499 500 return nil if @used_accesskeys.include?(key)
500 501 @used_accesskeys << key
501 502 key
502 503 end
503 504
504 505 # Formats text according to system settings.
505 506 # 2 ways to call this method:
506 507 # * with a String: textilizable(text, options)
507 508 # * with an object and one of its attribute: textilizable(issue, :description, options)
508 509 def textilizable(*args)
509 510 options = args.last.is_a?(Hash) ? args.pop : {}
510 511 case args.size
511 512 when 1
512 513 obj = options[:object]
513 514 text = args.shift
514 515 when 2
515 516 obj = args.shift
516 517 attr = args.shift
517 518 text = obj.send(attr).to_s
518 519 else
519 520 raise ArgumentError, 'invalid arguments to textilizable'
520 521 end
521 522 return '' if text.blank?
522 523 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
523 524 only_path = options.delete(:only_path) == false ? false : true
524 525
525 526 text = text.dup
526 527 macros = catch_macros(text)
527 528 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
528 529
529 530 @parsed_headings = []
530 531 @heading_anchors = {}
531 532 @current_section = 0 if options[:edit_section_links]
532 533
533 534 parse_sections(text, project, obj, attr, only_path, options)
534 535 text = parse_non_pre_blocks(text, obj, macros) do |text|
535 536 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
536 537 send method_name, text, project, obj, attr, only_path, options
537 538 end
538 539 end
539 540 parse_headings(text, project, obj, attr, only_path, options)
540 541
541 542 if @parsed_headings.any?
542 543 replace_toc(text, @parsed_headings)
543 544 end
544 545
545 546 text.html_safe
546 547 end
547 548
548 549 def parse_non_pre_blocks(text, obj, macros)
549 550 s = StringScanner.new(text)
550 551 tags = []
551 552 parsed = ''
552 553 while !s.eos?
553 554 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
554 555 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
555 556 if tags.empty?
556 557 yield text
557 558 inject_macros(text, obj, macros) if macros.any?
558 559 else
559 560 inject_macros(text, obj, macros, false) if macros.any?
560 561 end
561 562 parsed << text
562 563 if tag
563 564 if closing
564 565 if tags.last == tag.downcase
565 566 tags.pop
566 567 end
567 568 else
568 569 tags << tag.downcase
569 570 end
570 571 parsed << full_tag
571 572 end
572 573 end
573 574 # Close any non closing tags
574 575 while tag = tags.pop
575 576 parsed << "</#{tag}>"
576 577 end
577 578 parsed
578 579 end
579 580
580 581 def parse_inline_attachments(text, project, obj, attr, only_path, options)
581 582 # when using an image link, try to use an attachment, if possible
582 583 attachments = options[:attachments] || []
583 584 attachments += obj.attachments if obj.respond_to?(:attachments)
584 585 if attachments.present?
585 586 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
586 587 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
587 588 # search for the picture in attachments
588 589 if found = Attachment.latest_attach(attachments, filename)
589 590 image_url = download_named_attachment_path(found, found.filename, :only_path => only_path)
590 591 desc = found.description.to_s.gsub('"', '')
591 592 if !desc.blank? && alttext.blank?
592 593 alt = " title=\"#{desc}\" alt=\"#{desc}\""
593 594 end
594 595 "src=\"#{image_url}\"#{alt}"
595 596 else
596 597 m
597 598 end
598 599 end
599 600 end
600 601 end
601 602
602 603 # Wiki links
603 604 #
604 605 # Examples:
605 606 # [[mypage]]
606 607 # [[mypage|mytext]]
607 608 # wiki links can refer other project wikis, using project name or identifier:
608 609 # [[project:]] -> wiki starting page
609 610 # [[project:|mytext]]
610 611 # [[project:mypage]]
611 612 # [[project:mypage|mytext]]
612 613 def parse_wiki_links(text, project, obj, attr, only_path, options)
613 614 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
614 615 link_project = project
615 616 esc, all, page, title = $1, $2, $3, $5
616 617 if esc.nil?
617 618 if page =~ /^([^\:]+)\:(.*)$/
618 619 identifier, page = $1, $2
619 620 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
620 621 title ||= identifier if page.blank?
621 622 end
622 623
623 624 if link_project && link_project.wiki
624 625 # extract anchor
625 626 anchor = nil
626 627 if page =~ /^(.+?)\#(.+)$/
627 628 page, anchor = $1, $2
628 629 end
629 630 anchor = sanitize_anchor_name(anchor) if anchor.present?
630 631 # check if page exists
631 632 wiki_page = link_project.wiki.find_page(page)
632 633 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
633 634 "##{anchor}"
634 635 else
635 636 case options[:wiki_links]
636 637 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
637 638 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
638 639 else
639 640 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
640 641 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
641 642 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
642 643 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
643 644 end
644 645 end
645 646 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
646 647 else
647 648 # project or wiki doesn't exist
648 649 all
649 650 end
650 651 else
651 652 all
652 653 end
653 654 end
654 655 end
655 656
656 657 # Redmine links
657 658 #
658 659 # Examples:
659 660 # Issues:
660 661 # #52 -> Link to issue #52
661 662 # Changesets:
662 663 # r52 -> Link to revision 52
663 664 # commit:a85130f -> Link to scmid starting with a85130f
664 665 # Documents:
665 666 # document#17 -> Link to document with id 17
666 667 # document:Greetings -> Link to the document with title "Greetings"
667 668 # document:"Some document" -> Link to the document with title "Some document"
668 669 # Versions:
669 670 # version#3 -> Link to version with id 3
670 671 # version:1.0.0 -> Link to version named "1.0.0"
671 672 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
672 673 # Attachments:
673 674 # attachment:file.zip -> Link to the attachment of the current object named file.zip
674 675 # Source files:
675 676 # source:some/file -> Link to the file located at /some/file in the project's repository
676 677 # source:some/file@52 -> Link to the file's revision 52
677 678 # source:some/file#L120 -> Link to line 120 of the file
678 679 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
679 680 # export:some/file -> Force the download of the file
680 681 # Forum messages:
681 682 # message#1218 -> Link to message with id 1218
682 683 # Projects:
683 684 # project:someproject -> Link to project named "someproject"
684 685 # project#3 -> Link to project with id 3
685 686 #
686 687 # Links can refer other objects from other projects, using project identifier:
687 688 # identifier:r52
688 689 # identifier:document:"Some document"
689 690 # identifier:version:1.0.0
690 691 # identifier:source:some/file
691 692 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
692 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|
693 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
694 695 link = nil
695 696 project = default_project
696 697 if project_identifier
697 698 project = Project.visible.find_by_identifier(project_identifier)
698 699 end
699 700 if esc.nil?
700 701 if prefix.nil? && sep == 'r'
701 702 if project
702 703 repository = nil
703 704 if repo_identifier
704 705 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
705 706 else
706 707 repository = project.repository
707 708 end
708 709 # project.changesets.visible raises an SQL error because of a double join on repositories
709 710 if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier))
710 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},
711 712 :class => 'changeset',
712 713 :title => truncate_single_line(changeset.comments, :length => 100))
713 714 end
714 715 end
715 716 elsif sep == '#'
716 717 oid = identifier.to_i
717 718 case prefix
718 719 when nil
719 720 if oid.to_s == identifier && issue = Issue.visible.find_by_id(oid, :include => :status)
720 721 anchor = comment_id ? "note-#{comment_id}" : nil
721 722 link = link_to(h("##{oid}#{comment_suffix}"), {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor},
722 723 :class => issue.css_classes,
723 724 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
724 725 end
725 726 when 'document'
726 727 if document = Document.visible.find_by_id(oid)
727 728 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
728 729 :class => 'document'
729 730 end
730 731 when 'version'
731 732 if version = Version.visible.find_by_id(oid)
732 733 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
733 734 :class => 'version'
734 735 end
735 736 when 'message'
736 737 if message = Message.visible.find_by_id(oid, :include => :parent)
737 738 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
738 739 end
739 740 when 'forum'
740 741 if board = Board.visible.find_by_id(oid)
741 742 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
742 743 :class => 'board'
743 744 end
744 745 when 'news'
745 746 if news = News.visible.find_by_id(oid)
746 747 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
747 748 :class => 'news'
748 749 end
749 750 when 'project'
750 751 if p = Project.visible.find_by_id(oid)
751 752 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
752 753 end
753 754 end
754 755 elsif sep == ':'
755 756 # removes the double quotes if any
756 757 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
757 758 case prefix
758 759 when 'document'
759 760 if project && document = project.documents.visible.find_by_title(name)
760 761 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
761 762 :class => 'document'
762 763 end
763 764 when 'version'
764 765 if project && version = project.versions.visible.find_by_name(name)
765 766 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
766 767 :class => 'version'
767 768 end
768 769 when 'forum'
769 770 if project && board = project.boards.visible.find_by_name(name)
770 771 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
771 772 :class => 'board'
772 773 end
773 774 when 'news'
774 775 if project && news = project.news.visible.find_by_title(name)
775 776 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
776 777 :class => 'news'
777 778 end
778 779 when 'commit', 'source', 'export'
779 780 if project
780 781 repository = nil
781 782 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
782 783 repo_prefix, repo_identifier, name = $1, $2, $3
783 784 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
784 785 else
785 786 repository = project.repository
786 787 end
787 788 if prefix == 'commit'
788 789 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
789 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},
790 791 :class => 'changeset',
791 792 :title => truncate_single_line(changeset.comments, :length => 100)
792 793 end
793 794 else
794 795 if repository && User.current.allowed_to?(:browse_repository, project)
795 796 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
796 797 path, rev, anchor = $1, $3, $5
797 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,
798 799 :path => to_path_param(path),
799 800 :rev => rev,
800 801 :anchor => anchor},
801 802 :class => (prefix == 'export' ? 'source download' : 'source')
802 803 end
803 804 end
804 805 repo_prefix = nil
805 806 end
806 807 when 'attachment'
807 808 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
808 809 if attachments && attachment = Attachment.latest_attach(attachments, name)
809 810 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
810 811 end
811 812 when 'project'
812 813 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
813 814 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
814 815 end
815 816 end
816 817 end
817 818 end
818 819 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
819 820 end
820 821 end
821 822
822 823 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
823 824
824 825 def parse_sections(text, project, obj, attr, only_path, options)
825 826 return unless options[:edit_section_links]
826 827 text.gsub!(HEADING_RE) do
827 828 heading = $1
828 829 @current_section += 1
829 830 if @current_section > 1
830 831 content_tag('div',
831 832 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
832 833 :class => 'contextual',
833 834 :title => l(:button_edit_section)) + heading.html_safe
834 835 else
835 836 heading
836 837 end
837 838 end
838 839 end
839 840
840 841 # Headings and TOC
841 842 # Adds ids and links to headings unless options[:headings] is set to false
842 843 def parse_headings(text, project, obj, attr, only_path, options)
843 844 return if options[:headings] == false
844 845
845 846 text.gsub!(HEADING_RE) do
846 847 level, attrs, content = $2.to_i, $3, $4
847 848 item = strip_tags(content).strip
848 849 anchor = sanitize_anchor_name(item)
849 850 # used for single-file wiki export
850 851 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
851 852 @heading_anchors[anchor] ||= 0
852 853 idx = (@heading_anchors[anchor] += 1)
853 854 if idx > 1
854 855 anchor = "#{anchor}-#{idx}"
855 856 end
856 857 @parsed_headings << [level, anchor, item]
857 858 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
858 859 end
859 860 end
860 861
861 862 MACROS_RE = /(
862 863 (!)? # escaping
863 864 (
864 865 \{\{ # opening tag
865 866 ([\w]+) # macro name
866 867 (\(([^\n\r]*?)\))? # optional arguments
867 868 ([\n\r].*?[\n\r])? # optional block of text
868 869 \}\} # closing tag
869 870 )
870 871 )/mx unless const_defined?(:MACROS_RE)
871 872
872 873 MACRO_SUB_RE = /(
873 874 \{\{
874 875 macro\((\d+)\)
875 876 \}\}
876 877 )/x unless const_defined?(:MACRO_SUB_RE)
877 878
878 879 # Extracts macros from text
879 880 def catch_macros(text)
880 881 macros = {}
881 882 text.gsub!(MACROS_RE) do
882 883 all, macro = $1, $4.downcase
883 884 if macro_exists?(macro) || all =~ MACRO_SUB_RE
884 885 index = macros.size
885 886 macros[index] = all
886 887 "{{macro(#{index})}}"
887 888 else
888 889 all
889 890 end
890 891 end
891 892 macros
892 893 end
893 894
894 895 # Executes and replaces macros in text
895 896 def inject_macros(text, obj, macros, execute=true)
896 897 text.gsub!(MACRO_SUB_RE) do
897 898 all, index = $1, $2.to_i
898 899 orig = macros.delete(index)
899 900 if execute && orig && orig =~ MACROS_RE
900 901 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
901 902 if esc.nil?
902 903 h(exec_macro(macro, obj, args, block) || all)
903 904 else
904 905 h(all)
905 906 end
906 907 elsif orig
907 908 h(orig)
908 909 else
909 910 h(all)
910 911 end
911 912 end
912 913 end
913 914
914 915 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
915 916
916 917 # Renders the TOC with given headings
917 918 def replace_toc(text, headings)
918 919 text.gsub!(TOC_RE) do
919 920 # Keep only the 4 first levels
920 921 headings = headings.select{|level, anchor, item| level <= 4}
921 922 if headings.empty?
922 923 ''
923 924 else
924 925 div_class = 'toc'
925 926 div_class << ' right' if $1 == '>'
926 927 div_class << ' left' if $1 == '<'
927 928 out = "<ul class=\"#{div_class}\"><li>"
928 929 root = headings.map(&:first).min
929 930 current = root
930 931 started = false
931 932 headings.each do |level, anchor, item|
932 933 if level > current
933 934 out << '<ul><li>' * (level - current)
934 935 elsif level < current
935 936 out << "</li></ul>\n" * (current - level) + "</li><li>"
936 937 elsif started
937 938 out << '</li><li>'
938 939 end
939 940 out << "<a href=\"##{anchor}\">#{item}</a>"
940 941 current = level
941 942 started = true
942 943 end
943 944 out << '</li></ul>' * (current - root)
944 945 out << '</li></ul>'
945 946 end
946 947 end
947 948 end
948 949
949 950 # Same as Rails' simple_format helper without using paragraphs
950 951 def simple_format_without_paragraph(text)
951 952 text.to_s.
952 953 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
953 954 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
954 955 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
955 956 html_safe
956 957 end
957 958
958 959 def lang_options_for_select(blank=true)
959 960 (blank ? [["(auto)", ""]] : []) + languages_options
960 961 end
961 962
962 963 def label_tag_for(name, option_tags = nil, options = {})
963 964 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
964 965 content_tag("label", label_text)
965 966 end
966 967
967 968 def labelled_form_for(*args, &proc)
968 969 args << {} unless args.last.is_a?(Hash)
969 970 options = args.last
970 971 if args.first.is_a?(Symbol)
971 972 options.merge!(:as => args.shift)
972 973 end
973 974 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
974 975 form_for(*args, &proc)
975 976 end
976 977
977 978 def labelled_fields_for(*args, &proc)
978 979 args << {} unless args.last.is_a?(Hash)
979 980 options = args.last
980 981 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
981 982 fields_for(*args, &proc)
982 983 end
983 984
984 985 def labelled_remote_form_for(*args, &proc)
985 986 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_remote_form_for is deprecated and will be removed in Redmine 2.2."
986 987 args << {} unless args.last.is_a?(Hash)
987 988 options = args.last
988 989 options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
989 990 form_for(*args, &proc)
990 991 end
991 992
992 993 def error_messages_for(*objects)
993 994 html = ""
994 995 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
995 996 errors = objects.map {|o| o.errors.full_messages}.flatten
996 997 if errors.any?
997 998 html << "<div id='errorExplanation'><ul>\n"
998 999 errors.each do |error|
999 1000 html << "<li>#{h error}</li>\n"
1000 1001 end
1001 1002 html << "</ul></div>\n"
1002 1003 end
1003 1004 html.html_safe
1004 1005 end
1005 1006
1006 1007 def delete_link(url, options={})
1007 1008 options = {
1008 1009 :method => :delete,
1009 1010 :data => {:confirm => l(:text_are_you_sure)},
1010 1011 :class => 'icon icon-del'
1011 1012 }.merge(options)
1012 1013
1013 1014 link_to l(:button_delete), url, options
1014 1015 end
1015 1016
1016 1017 def preview_link(url, form, target='preview', options={})
1017 1018 content_tag 'a', l(:label_preview), {
1018 1019 :href => "#",
1019 1020 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1020 1021 :accesskey => accesskey(:preview)
1021 1022 }.merge(options)
1022 1023 end
1023 1024
1024 1025 def link_to_function(name, function, html_options={})
1025 1026 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1026 1027 end
1027 1028
1028 1029 # Helper to render JSON in views
1029 1030 def raw_json(arg)
1030 1031 arg.to_json.to_s.gsub('/', '\/').html_safe
1031 1032 end
1032 1033
1033 1034 def back_url
1034 1035 url = params[:back_url]
1035 1036 if url.nil? && referer = request.env['HTTP_REFERER']
1036 1037 url = CGI.unescape(referer.to_s)
1037 1038 end
1038 1039 url
1039 1040 end
1040 1041
1041 1042 def back_url_hidden_field_tag
1042 1043 url = back_url
1043 1044 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1044 1045 end
1045 1046
1046 1047 def check_all_links(form_name)
1047 1048 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1048 1049 " | ".html_safe +
1049 1050 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1050 1051 end
1051 1052
1052 1053 def progress_bar(pcts, options={})
1053 1054 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1054 1055 pcts = pcts.collect(&:round)
1055 1056 pcts[1] = pcts[1] - pcts[0]
1056 1057 pcts << (100 - pcts[1] - pcts[0])
1057 1058 width = options[:width] || '100px;'
1058 1059 legend = options[:legend] || ''
1059 1060 content_tag('table',
1060 1061 content_tag('tr',
1061 1062 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1062 1063 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1063 1064 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1064 1065 ), :class => 'progress', :style => "width: #{width};").html_safe +
1065 1066 content_tag('p', legend, :class => 'percent').html_safe
1066 1067 end
1067 1068
1068 1069 def checked_image(checked=true)
1069 1070 if checked
1070 1071 image_tag 'toggle_check.png'
1071 1072 end
1072 1073 end
1073 1074
1074 1075 def context_menu(url)
1075 1076 unless @context_menu_included
1076 1077 content_for :header_tags do
1077 1078 javascript_include_tag('context_menu') +
1078 1079 stylesheet_link_tag('context_menu')
1079 1080 end
1080 1081 if l(:direction) == 'rtl'
1081 1082 content_for :header_tags do
1082 1083 stylesheet_link_tag('context_menu_rtl')
1083 1084 end
1084 1085 end
1085 1086 @context_menu_included = true
1086 1087 end
1087 1088 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1088 1089 end
1089 1090
1090 1091 def calendar_for(field_id)
1091 1092 include_calendar_headers_tags
1092 1093 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1093 1094 end
1094 1095
1095 1096 def include_calendar_headers_tags
1096 1097 unless @calendar_headers_tags_included
1097 1098 tags = javascript_include_tag("datepicker")
1098 1099 @calendar_headers_tags_included = true
1099 1100 content_for :header_tags do
1100 1101 start_of_week = Setting.start_of_week
1101 1102 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1102 1103 # Redmine uses 1..7 (monday..sunday) in settings and locales
1103 1104 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1104 1105 start_of_week = start_of_week.to_i % 7
1105 1106 tags << javascript_tag(
1106 1107 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1107 1108 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1108 1109 path_to_image('/images/calendar.png') +
1109 1110 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1110 1111 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1111 1112 "beforeShow: beforeShowDatePicker};")
1112 1113 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1113 1114 unless jquery_locale == 'en'
1114 1115 tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js")
1115 1116 end
1116 1117 tags
1117 1118 end
1118 1119 end
1119 1120 end
1120 1121
1121 1122 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1122 1123 # Examples:
1123 1124 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1124 1125 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1125 1126 #
1126 1127 def stylesheet_link_tag(*sources)
1127 1128 options = sources.last.is_a?(Hash) ? sources.pop : {}
1128 1129 plugin = options.delete(:plugin)
1129 1130 sources = sources.map do |source|
1130 1131 if plugin
1131 1132 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1132 1133 elsif current_theme && current_theme.stylesheets.include?(source)
1133 1134 current_theme.stylesheet_path(source)
1134 1135 else
1135 1136 source
1136 1137 end
1137 1138 end
1138 1139 super sources, options
1139 1140 end
1140 1141
1141 1142 # Overrides Rails' image_tag with themes and plugins support.
1142 1143 # Examples:
1143 1144 # image_tag('image.png') # => picks image.png from the current theme or defaults
1144 1145 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1145 1146 #
1146 1147 def image_tag(source, options={})
1147 1148 if plugin = options.delete(:plugin)
1148 1149 source = "/plugin_assets/#{plugin}/images/#{source}"
1149 1150 elsif current_theme && current_theme.images.include?(source)
1150 1151 source = current_theme.image_path(source)
1151 1152 end
1152 1153 super source, options
1153 1154 end
1154 1155
1155 1156 # Overrides Rails' javascript_include_tag with plugins support
1156 1157 # Examples:
1157 1158 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1158 1159 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1159 1160 #
1160 1161 def javascript_include_tag(*sources)
1161 1162 options = sources.last.is_a?(Hash) ? sources.pop : {}
1162 1163 if plugin = options.delete(:plugin)
1163 1164 sources = sources.map do |source|
1164 1165 if plugin
1165 1166 "/plugin_assets/#{plugin}/javascripts/#{source}"
1166 1167 else
1167 1168 source
1168 1169 end
1169 1170 end
1170 1171 end
1171 1172 super sources, options
1172 1173 end
1173 1174
1174 1175 def content_for(name, content = nil, &block)
1175 1176 @has_content ||= {}
1176 1177 @has_content[name] = true
1177 1178 super(name, content, &block)
1178 1179 end
1179 1180
1180 1181 def has_content?(name)
1181 1182 (@has_content && @has_content[name]) || false
1182 1183 end
1183 1184
1184 1185 def sidebar_content?
1185 1186 has_content?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1186 1187 end
1187 1188
1188 1189 def view_layouts_base_sidebar_hook_response
1189 1190 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1190 1191 end
1191 1192
1192 1193 def email_delivery_enabled?
1193 1194 !!ActionMailer::Base.perform_deliveries
1194 1195 end
1195 1196
1196 1197 # Returns the avatar image tag for the given +user+ if avatars are enabled
1197 1198 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1198 1199 def avatar(user, options = { })
1199 1200 if Setting.gravatar_enabled?
1200 1201 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1201 1202 email = nil
1202 1203 if user.respond_to?(:mail)
1203 1204 email = user.mail
1204 1205 elsif user.to_s =~ %r{<(.+?)>}
1205 1206 email = $1
1206 1207 end
1207 1208 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1208 1209 else
1209 1210 ''
1210 1211 end
1211 1212 end
1212 1213
1213 1214 def sanitize_anchor_name(anchor)
1214 1215 if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java'
1215 1216 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1216 1217 else
1217 1218 # TODO: remove when ruby1.8 is no longer supported
1218 1219 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1219 1220 end
1220 1221 end
1221 1222
1222 1223 # Returns the javascript tags that are included in the html layout head
1223 1224 def javascript_heads
1224 1225 tags = javascript_include_tag('jquery-1.8.3-ui-1.9.2-ujs-2.0.3', 'application')
1225 1226 unless User.current.pref.warn_on_leaving_unsaved == '0'
1226 1227 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1227 1228 end
1228 1229 tags
1229 1230 end
1230 1231
1231 1232 def favicon
1232 1233 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1233 1234 end
1234 1235
1235 1236 def robot_exclusion_tag
1236 1237 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1237 1238 end
1238 1239
1239 1240 # Returns true if arg is expected in the API response
1240 1241 def include_in_api_response?(arg)
1241 1242 unless @included_in_api_response
1242 1243 param = params[:include]
1243 1244 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1244 1245 @included_in_api_response.collect!(&:strip)
1245 1246 end
1246 1247 @included_in_api_response.include?(arg.to_s)
1247 1248 end
1248 1249
1249 1250 # Returns options or nil if nometa param or X-Redmine-Nometa header
1250 1251 # was set in the request
1251 1252 def api_meta(options)
1252 1253 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1253 1254 # compatibility mode for activeresource clients that raise
1254 1255 # an error when unserializing an array with attributes
1255 1256 nil
1256 1257 else
1257 1258 options
1258 1259 end
1259 1260 end
1260 1261
1261 1262 private
1262 1263
1263 1264 def wiki_helper
1264 1265 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1265 1266 extend helper
1266 1267 return self
1267 1268 end
1268 1269
1269 1270 def link_to_content_update(text, url_params = {}, html_options = {})
1270 1271 link_to(text, url_params, html_options)
1271 1272 end
1272 1273 end
@@ -1,592 +1,597
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 323 def test_show_should_not_display_hidden_custom_fields
324 324 ProjectCustomField.find_by_name('Development status').update_attribute :visible, false
325 325 get :show, :id => 'ecookbook'
326 326 assert_response :success
327 327 assert_template 'show'
328 328 assert_not_nil assigns(:project)
329 329
330 330 assert_select 'li', :text => /Development status/, :count => 0
331 331 end
332 332
333 333 def test_show_should_not_fail_when_custom_values_are_nil
334 334 project = Project.find_by_identifier('ecookbook')
335 335 project.custom_values.first.update_attribute(:value, nil)
336 336 get :show, :id => 'ecookbook'
337 337 assert_response :success
338 338 assert_template 'show'
339 339 assert_not_nil assigns(:project)
340 340 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
341 341 end
342 342
343 343 def show_archived_project_should_be_denied
344 344 project = Project.find_by_identifier('ecookbook')
345 345 project.archive!
346 346
347 347 get :show, :id => 'ecookbook'
348 348 assert_response 403
349 349 assert_nil assigns(:project)
350 350 assert_select 'p', :text => /archived/
351 351 end
352 352
353 353 def test_show_should_not_show_private_subprojects_that_are_not_visible
354 354 get :show, :id => 'ecookbook'
355 355 assert_response :success
356 356 assert_template 'show'
357 357 assert_select 'a', :text => /Private child/, :count => 0
358 358 end
359 359
360 360 def test_show_should_show_private_subprojects_that_are_visible
361 361 @request.session[:user_id] = 2 # manager who is a member of the private subproject
362 362 get :show, :id => 'ecookbook'
363 363 assert_response :success
364 364 assert_template 'show'
365 365 assert_select 'a', :text => /Private child/
366 366 end
367 367
368 368 def test_settings
369 369 @request.session[:user_id] = 2 # manager
370 370 get :settings, :id => 1
371 371 assert_response :success
372 372 assert_template 'settings'
373 373 end
374 374
375 375 def test_settings_of_subproject
376 376 @request.session[:user_id] = 2
377 377 get :settings, :id => 'private-child'
378 378 assert_response :success
379 379 assert_template 'settings'
380 380
381 381 assert_select 'input[type=checkbox][name=?]', 'project[inherit_members]'
382 382 end
383 383
384 384 def test_settings_should_be_denied_for_member_on_closed_project
385 385 Project.find(1).close
386 386 @request.session[:user_id] = 2 # manager
387 387
388 388 get :settings, :id => 1
389 389 assert_response 403
390 390 end
391 391
392 392 def test_settings_should_be_denied_for_anonymous_on_closed_project
393 393 Project.find(1).close
394 394
395 395 get :settings, :id => 1
396 396 assert_response 302
397 397 end
398 398
399 399 def test_update
400 400 @request.session[:user_id] = 2 # manager
401 401 post :update, :id => 1, :project => {:name => 'Test changed name',
402 402 :issue_custom_field_ids => ['']}
403 403 assert_redirected_to '/projects/ecookbook/settings'
404 404 project = Project.find(1)
405 405 assert_equal 'Test changed name', project.name
406 406 end
407 407
408 408 def test_update_with_failure
409 409 @request.session[:user_id] = 2 # manager
410 410 post :update, :id => 1, :project => {:name => ''}
411 411 assert_response :success
412 412 assert_template 'settings'
413 413 assert_error_tag :content => /name can&#x27;t be blank/i
414 414 end
415 415
416 416 def test_update_should_be_denied_for_member_on_closed_project
417 417 Project.find(1).close
418 418 @request.session[:user_id] = 2 # manager
419 419
420 420 post :update, :id => 1, :project => {:name => 'Closed'}
421 421 assert_response 403
422 422 assert_equal 'eCookbook', Project.find(1).name
423 423 end
424 424
425 425 def test_update_should_be_denied_for_anonymous_on_closed_project
426 426 Project.find(1).close
427 427
428 428 post :update, :id => 1, :project => {:name => 'Closed'}
429 429 assert_response 302
430 430 assert_equal 'eCookbook', Project.find(1).name
431 431 end
432 432
433 433 def test_modules
434 434 @request.session[:user_id] = 2
435 435 Project.find(1).enabled_module_names = ['issue_tracking', 'news']
436 436
437 437 post :modules, :id => 1, :enabled_module_names => ['issue_tracking', 'repository', 'documents']
438 438 assert_redirected_to '/projects/ecookbook/settings/modules'
439 439 assert_equal ['documents', 'issue_tracking', 'repository'], Project.find(1).enabled_module_names.sort
440 440 end
441 441
442 442 def test_destroy_leaf_project_without_confirmation_should_show_confirmation
443 443 @request.session[:user_id] = 1 # admin
444 444
445 445 assert_no_difference 'Project.count' do
446 446 delete :destroy, :id => 2
447 447 assert_response :success
448 448 assert_template 'destroy'
449 449 end
450 450 end
451 451
452 452 def test_destroy_without_confirmation_should_show_confirmation_with_subprojects
453 453 @request.session[:user_id] = 1 # admin
454 454
455 455 assert_no_difference 'Project.count' do
456 456 delete :destroy, :id => 1
457 457 assert_response :success
458 458 assert_template 'destroy'
459 459 end
460 460 assert_select 'strong',
461 461 :text => ['Private child of eCookbook',
462 462 'Child of private child, eCookbook Subproject 1',
463 463 'eCookbook Subproject 2'].join(', ')
464 464 end
465 465
466 466 def test_destroy_with_confirmation_should_destroy_the_project_and_subprojects
467 467 @request.session[:user_id] = 1 # admin
468 468
469 469 assert_difference 'Project.count', -5 do
470 470 delete :destroy, :id => 1, :confirm => 1
471 471 assert_redirected_to '/admin/projects'
472 472 end
473 473 assert_nil Project.find_by_id(1)
474 474 end
475 475
476 476 def test_archive
477 477 @request.session[:user_id] = 1 # admin
478 478 post :archive, :id => 1
479 479 assert_redirected_to '/admin/projects'
480 480 assert !Project.find(1).active?
481 481 end
482 482
483 483 def test_archive_with_failure
484 484 @request.session[:user_id] = 1
485 485 Project.any_instance.stubs(:archive).returns(false)
486 486 post :archive, :id => 1
487 487 assert_redirected_to '/admin/projects'
488 488 assert_match /project cannot be archived/i, flash[:error]
489 489 end
490 490
491 491 def test_unarchive
492 492 @request.session[:user_id] = 1 # admin
493 493 Project.find(1).archive
494 494 post :unarchive, :id => 1
495 495 assert_redirected_to '/admin/projects'
496 496 assert Project.find(1).active?
497 497 end
498 498
499 499 def test_close
500 500 @request.session[:user_id] = 2
501 501 post :close, :id => 1
502 502 assert_redirected_to '/projects/ecookbook'
503 503 assert_equal Project::STATUS_CLOSED, Project.find(1).status
504 504 end
505 505
506 506 def test_reopen
507 507 Project.find(1).close
508 508 @request.session[:user_id] = 2
509 509 post :reopen, :id => 1
510 510 assert_redirected_to '/projects/ecookbook'
511 511 assert Project.find(1).active?
512 512 end
513 513
514 514 def test_project_breadcrumbs_should_be_limited_to_3_ancestors
515 515 CustomField.delete_all
516 516 parent = nil
517 517 6.times do |i|
518 518 p = Project.generate_with_parent!(parent)
519 519 get :show, :id => p
520 520 assert_select '#header h1' do
521 521 assert_select 'a', :count => [i, 3].min
522 522 end
523 523
524 524 parent = p
525 525 end
526 526 end
527 527
528 528 def test_get_copy
529 529 @request.session[:user_id] = 1 # admin
530 530 get :copy, :id => 1
531 531 assert_response :success
532 532 assert_template 'copy'
533 533 assert assigns(:project)
534 534 assert_equal Project.find(1).description, assigns(:project).description
535 535 assert_nil assigns(:project).id
536 536
537 537 assert_select 'input[name=?][value=?]', 'project[enabled_module_names][]', 'issue_tracking', 1
538 538 end
539 539
540 540 def test_get_copy_with_invalid_source_should_respond_with_404
541 541 @request.session[:user_id] = 1
542 542 get :copy, :id => 99
543 543 assert_response 404
544 544 end
545 545
546 546 def test_post_copy_should_copy_requested_items
547 547 @request.session[:user_id] = 1 # admin
548 548 CustomField.delete_all
549 549
550 550 assert_difference 'Project.count' do
551 551 post :copy, :id => 1,
552 552 :project => {
553 553 :name => 'Copy',
554 554 :identifier => 'unique-copy',
555 555 :tracker_ids => ['1', '2', '3', ''],
556 556 :enabled_module_names => %w(issue_tracking time_tracking)
557 557 },
558 558 :only => %w(issues versions)
559 559 end
560 560 project = Project.find('unique-copy')
561 561 source = Project.find(1)
562 562 assert_equal %w(issue_tracking time_tracking), project.enabled_module_names.sort
563 563
564 564 assert_equal source.versions.count, project.versions.count, "All versions were not copied"
565 565 assert_equal source.issues.count, project.issues.count, "All issues were not copied"
566 566 assert_equal 0, project.members.count
567 567 end
568 568
569 569 def test_post_copy_should_redirect_to_settings_when_successful
570 570 @request.session[:user_id] = 1 # admin
571 571 post :copy, :id => 1, :project => {:name => 'Copy', :identifier => 'unique-copy'}
572 572 assert_response :redirect
573 573 assert_redirected_to :controller => 'projects', :action => 'settings', :id => 'unique-copy'
574 574 end
575 575
576 576 def test_jump_should_redirect_to_active_tab
577 577 get :show, :id => 1, :jump => 'issues'
578 578 assert_redirected_to '/projects/ecookbook/issues'
579 579 end
580 580
581 581 def test_jump_should_not_redirect_to_inactive_tab
582 582 get :show, :id => 3, :jump => 'documents'
583 583 assert_response :success
584 584 assert_template 'show'
585 585 end
586 586
587 587 def test_jump_should_not_redirect_to_unknown_tab
588 588 get :show, :id => 3, :jump => 'foobar'
589 589 assert_response :success
590 590 assert_template 'show'
591 591 end
592
593 def test_body_should_have_project_css_class
594 get :show, :id => 1
595 assert_select 'body.project-ecookbook'
596 end
592 597 end
General Comments 0
You need to be logged in to leave comments. Login now