##// END OF EJS Templates
Fixed that links for relations in notifications do not include hostname (#15677)....
Jean-Philippe Lang -
r12140:13756eb3a830
parent child
Show More
@@ -1,1310 +1,1311
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 s = link_to text, issue_path(issue), :class => issue.css_classes, :title => title
82 only_path = options[:only_path].nil? ? true : options[:only_path]
83 s = link_to text, issue_path(issue, :only_path => only_path), :class => issue.css_classes, :title => title
83 84 s << h(": #{subject}") if subject
84 85 s = h("#{issue.project} - ") + s if options[:project]
85 86 s
86 87 end
87 88
88 89 # Generates a link to an attachment.
89 90 # Options:
90 91 # * :text - Link text (default to attachment filename)
91 92 # * :download - Force download (default: false)
92 93 def link_to_attachment(attachment, options={})
93 94 text = options.delete(:text) || attachment.filename
94 95 route_method = options.delete(:download) ? :download_named_attachment_path : :named_attachment_path
95 96 html_options = options.slice!(:only_path)
96 97 url = send(route_method, attachment, attachment.filename, options)
97 98 link_to text, url, html_options
98 99 end
99 100
100 101 # Generates a link to a SCM revision
101 102 # Options:
102 103 # * :text - Link text (default to the formatted revision)
103 104 def link_to_revision(revision, repository, options={})
104 105 if repository.is_a?(Project)
105 106 repository = repository.repository
106 107 end
107 108 text = options.delete(:text) || format_revision(revision)
108 109 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
109 110 link_to(
110 111 h(text),
111 112 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
112 113 :title => l(:label_revision_id, format_revision(revision))
113 114 )
114 115 end
115 116
116 117 # Generates a link to a message
117 118 def link_to_message(message, options={}, html_options = nil)
118 119 link_to(
119 120 truncate(message.subject, :length => 60),
120 121 board_message_path(message.board_id, message.parent_id || message.id, {
121 122 :r => (message.parent_id && message.id),
122 123 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
123 124 }.merge(options)),
124 125 html_options
125 126 )
126 127 end
127 128
128 129 # Generates a link to a project if active
129 130 # Examples:
130 131 #
131 132 # link_to_project(project) # => link to the specified project overview
132 133 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
133 134 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
134 135 #
135 136 def link_to_project(project, options={}, html_options = nil)
136 137 if project.archived?
137 138 h(project.name)
138 139 elsif options.key?(:action)
139 140 ActiveSupport::Deprecation.warn "#link_to_project with :action option is deprecated and will be removed in Redmine 3.0."
140 141 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
141 142 link_to project.name, url, html_options
142 143 else
143 144 link_to project.name, project_path(project, options), html_options
144 145 end
145 146 end
146 147
147 148 # Generates a link to a project settings if active
148 149 def link_to_project_settings(project, options={}, html_options=nil)
149 150 if project.active?
150 151 link_to project.name, settings_project_path(project, options), html_options
151 152 elsif project.archived?
152 153 h(project.name)
153 154 else
154 155 link_to project.name, project_path(project, options), html_options
155 156 end
156 157 end
157 158
158 159 # Helper that formats object for html or text rendering
159 160 def format_object(object, html=true)
160 161 case object.class.name
161 162 when 'Array'
162 163 object.map {|o| format_object(o, html)}.join(', ').html_safe
163 164 when 'Time'
164 165 format_time(object)
165 166 when 'Date'
166 167 format_date(object)
167 168 when 'Fixnum'
168 169 object.to_s
169 170 when 'Float'
170 171 sprintf "%.2f", object
171 172 when 'User'
172 173 html ? link_to_user(object) : object.to_s
173 174 when 'Project'
174 175 html ? link_to_project(object) : object.to_s
175 176 when 'Version'
176 177 html ? link_to(object.name, version_path(object)) : object.to_s
177 178 when 'TrueClass'
178 179 l(:general_text_Yes)
179 180 when 'FalseClass'
180 181 l(:general_text_No)
181 182 when 'Issue'
182 183 object.visible? && html ? link_to_issue(object) : "##{object.id}"
183 184 when 'CustomValue', 'CustomFieldValue'
184 185 if object.custom_field
185 186 f = object.custom_field.format.formatted_custom_value(self, object, html)
186 187 if f.nil? || f.is_a?(String)
187 188 f
188 189 else
189 190 format_object(f, html)
190 191 end
191 192 else
192 193 object.value.to_s
193 194 end
194 195 else
195 196 html ? h(object) : object.to_s
196 197 end
197 198 end
198 199
199 200 def wiki_page_path(page, options={})
200 201 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
201 202 end
202 203
203 204 def thumbnail_tag(attachment)
204 205 link_to image_tag(thumbnail_path(attachment)),
205 206 named_attachment_path(attachment, attachment.filename),
206 207 :title => attachment.filename
207 208 end
208 209
209 210 def toggle_link(name, id, options={})
210 211 onclick = "$('##{id}').toggle(); "
211 212 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
212 213 onclick << "return false;"
213 214 link_to(name, "#", :onclick => onclick)
214 215 end
215 216
216 217 def image_to_function(name, function, html_options = {})
217 218 html_options.symbolize_keys!
218 219 tag(:input, html_options.merge({
219 220 :type => "image", :src => image_path(name),
220 221 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
221 222 }))
222 223 end
223 224
224 225 def format_activity_title(text)
225 226 h(truncate_single_line(text, :length => 100))
226 227 end
227 228
228 229 def format_activity_day(date)
229 230 date == User.current.today ? l(:label_today).titleize : format_date(date)
230 231 end
231 232
232 233 def format_activity_description(text)
233 234 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
234 235 ).gsub(/[\r\n]+/, "<br />").html_safe
235 236 end
236 237
237 238 def format_version_name(version)
238 239 if version.project == @project
239 240 h(version)
240 241 else
241 242 h("#{version.project} - #{version}")
242 243 end
243 244 end
244 245
245 246 def due_date_distance_in_words(date)
246 247 if date
247 248 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
248 249 end
249 250 end
250 251
251 252 # Renders a tree of projects as a nested set of unordered lists
252 253 # The given collection may be a subset of the whole project tree
253 254 # (eg. some intermediate nodes are private and can not be seen)
254 255 def render_project_nested_lists(projects)
255 256 s = ''
256 257 if projects.any?
257 258 ancestors = []
258 259 original_project = @project
259 260 projects.sort_by(&:lft).each do |project|
260 261 # set the project environment to please macros.
261 262 @project = project
262 263 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
263 264 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
264 265 else
265 266 ancestors.pop
266 267 s << "</li>"
267 268 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
268 269 ancestors.pop
269 270 s << "</ul></li>\n"
270 271 end
271 272 end
272 273 classes = (ancestors.empty? ? 'root' : 'child')
273 274 s << "<li class='#{classes}'><div class='#{classes}'>"
274 275 s << h(block_given? ? yield(project) : project.name)
275 276 s << "</div>\n"
276 277 ancestors << project
277 278 end
278 279 s << ("</li></ul>\n" * ancestors.size)
279 280 @project = original_project
280 281 end
281 282 s.html_safe
282 283 end
283 284
284 285 def render_page_hierarchy(pages, node=nil, options={})
285 286 content = ''
286 287 if pages[node]
287 288 content << "<ul class=\"pages-hierarchy\">\n"
288 289 pages[node].each do |page|
289 290 content << "<li>"
290 291 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
291 292 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
292 293 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
293 294 content << "</li>\n"
294 295 end
295 296 content << "</ul>\n"
296 297 end
297 298 content.html_safe
298 299 end
299 300
300 301 # Renders flash messages
301 302 def render_flash_messages
302 303 s = ''
303 304 flash.each do |k,v|
304 305 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
305 306 end
306 307 s.html_safe
307 308 end
308 309
309 310 # Renders tabs and their content
310 311 def render_tabs(tabs)
311 312 if tabs.any?
312 313 render :partial => 'common/tabs', :locals => {:tabs => tabs}
313 314 else
314 315 content_tag 'p', l(:label_no_data), :class => "nodata"
315 316 end
316 317 end
317 318
318 319 # Renders the project quick-jump box
319 320 def render_project_jump_box
320 321 return unless User.current.logged?
321 322 projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
322 323 if projects.any?
323 324 options =
324 325 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
325 326 '<option value="" disabled="disabled">---</option>').html_safe
326 327
327 328 options << project_tree_options_for_select(projects, :selected => @project) do |p|
328 329 { :value => project_path(:id => p, :jump => current_menu_item) }
329 330 end
330 331
331 332 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
332 333 end
333 334 end
334 335
335 336 def project_tree_options_for_select(projects, options = {})
336 337 s = ''
337 338 project_tree(projects) do |project, level|
338 339 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
339 340 tag_options = {:value => project.id}
340 341 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
341 342 tag_options[:selected] = 'selected'
342 343 else
343 344 tag_options[:selected] = nil
344 345 end
345 346 tag_options.merge!(yield(project)) if block_given?
346 347 s << content_tag('option', name_prefix + h(project), tag_options)
347 348 end
348 349 s.html_safe
349 350 end
350 351
351 352 # Yields the given block for each project with its level in the tree
352 353 #
353 354 # Wrapper for Project#project_tree
354 355 def project_tree(projects, &block)
355 356 Project.project_tree(projects, &block)
356 357 end
357 358
358 359 def principals_check_box_tags(name, principals)
359 360 s = ''
360 361 principals.each do |principal|
361 362 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
362 363 end
363 364 s.html_safe
364 365 end
365 366
366 367 # Returns a string for users/groups option tags
367 368 def principals_options_for_select(collection, selected=nil)
368 369 s = ''
369 370 if collection.include?(User.current)
370 371 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
371 372 end
372 373 groups = ''
373 374 collection.sort.each do |element|
374 375 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
375 376 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
376 377 end
377 378 unless groups.empty?
378 379 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
379 380 end
380 381 s.html_safe
381 382 end
382 383
383 384 # Options for the new membership projects combo-box
384 385 def options_for_membership_project_select(principal, projects)
385 386 options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
386 387 options << project_tree_options_for_select(projects) do |p|
387 388 {:disabled => principal.projects.to_a.include?(p)}
388 389 end
389 390 options
390 391 end
391 392
392 393 def option_tag(name, text, value, selected=nil, options={})
393 394 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
394 395 end
395 396
396 397 # Truncates and returns the string as a single line
397 398 def truncate_single_line(string, *args)
398 399 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
399 400 end
400 401
401 402 # Truncates at line break after 250 characters or options[:length]
402 403 def truncate_lines(string, options={})
403 404 length = options[:length] || 250
404 405 if string.to_s =~ /\A(.{#{length}}.*?)$/m
405 406 "#{$1}..."
406 407 else
407 408 string
408 409 end
409 410 end
410 411
411 412 def anchor(text)
412 413 text.to_s.gsub(' ', '_')
413 414 end
414 415
415 416 def html_hours(text)
416 417 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
417 418 end
418 419
419 420 def authoring(created, author, options={})
420 421 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
421 422 end
422 423
423 424 def time_tag(time)
424 425 text = distance_of_time_in_words(Time.now, time)
425 426 if @project
426 427 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
427 428 else
428 429 content_tag('abbr', text, :title => format_time(time))
429 430 end
430 431 end
431 432
432 433 def syntax_highlight_lines(name, content)
433 434 lines = []
434 435 syntax_highlight(name, content).each_line { |line| lines << line }
435 436 lines
436 437 end
437 438
438 439 def syntax_highlight(name, content)
439 440 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
440 441 end
441 442
442 443 def to_path_param(path)
443 444 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
444 445 str.blank? ? nil : str
445 446 end
446 447
447 448 def reorder_links(name, url, method = :post)
448 449 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
449 450 url.merge({"#{name}[move_to]" => 'highest'}),
450 451 :method => method, :title => l(:label_sort_highest)) +
451 452 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
452 453 url.merge({"#{name}[move_to]" => 'higher'}),
453 454 :method => method, :title => l(:label_sort_higher)) +
454 455 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
455 456 url.merge({"#{name}[move_to]" => 'lower'}),
456 457 :method => method, :title => l(:label_sort_lower)) +
457 458 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
458 459 url.merge({"#{name}[move_to]" => 'lowest'}),
459 460 :method => method, :title => l(:label_sort_lowest))
460 461 end
461 462
462 463 def breadcrumb(*args)
463 464 elements = args.flatten
464 465 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
465 466 end
466 467
467 468 def other_formats_links(&block)
468 469 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
469 470 yield Redmine::Views::OtherFormatsBuilder.new(self)
470 471 concat('</p>'.html_safe)
471 472 end
472 473
473 474 def page_header_title
474 475 if @project.nil? || @project.new_record?
475 476 h(Setting.app_title)
476 477 else
477 478 b = []
478 479 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
479 480 if ancestors.any?
480 481 root = ancestors.shift
481 482 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
482 483 if ancestors.size > 2
483 484 b << "\xe2\x80\xa6"
484 485 ancestors = ancestors[-2, 2]
485 486 end
486 487 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
487 488 end
488 489 b << h(@project)
489 490 b.join(" \xc2\xbb ").html_safe
490 491 end
491 492 end
492 493
493 494 # Returns a h2 tag and sets the html title with the given arguments
494 495 def title(*args)
495 496 strings = args.map do |arg|
496 497 if arg.is_a?(Array) && arg.size >= 2
497 498 link_to(*arg)
498 499 else
499 500 h(arg.to_s)
500 501 end
501 502 end
502 503 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
503 504 content_tag('h2', strings.join(' &#187; ').html_safe)
504 505 end
505 506
506 507 # Sets the html title
507 508 # Returns the html title when called without arguments
508 509 # Current project name and app_title and automatically appended
509 510 # Exemples:
510 511 # html_title 'Foo', 'Bar'
511 512 # html_title # => 'Foo - Bar - My Project - Redmine'
512 513 def html_title(*args)
513 514 if args.empty?
514 515 title = @html_title || []
515 516 title << @project.name if @project
516 517 title << Setting.app_title unless Setting.app_title == title.last
517 518 title.reject(&:blank?).join(' - ')
518 519 else
519 520 @html_title ||= []
520 521 @html_title += args
521 522 end
522 523 end
523 524
524 525 # Returns the theme, controller name, and action as css classes for the
525 526 # HTML body.
526 527 def body_css_classes
527 528 css = []
528 529 if theme = Redmine::Themes.theme(Setting.ui_theme)
529 530 css << 'theme-' + theme.name
530 531 end
531 532
532 533 css << 'project-' + @project.identifier if @project && @project.identifier.present?
533 534 css << 'controller-' + controller_name
534 535 css << 'action-' + action_name
535 536 css.join(' ')
536 537 end
537 538
538 539 def accesskey(s)
539 540 @used_accesskeys ||= []
540 541 key = Redmine::AccessKeys.key_for(s)
541 542 return nil if @used_accesskeys.include?(key)
542 543 @used_accesskeys << key
543 544 key
544 545 end
545 546
546 547 # Formats text according to system settings.
547 548 # 2 ways to call this method:
548 549 # * with a String: textilizable(text, options)
549 550 # * with an object and one of its attribute: textilizable(issue, :description, options)
550 551 def textilizable(*args)
551 552 options = args.last.is_a?(Hash) ? args.pop : {}
552 553 case args.size
553 554 when 1
554 555 obj = options[:object]
555 556 text = args.shift
556 557 when 2
557 558 obj = args.shift
558 559 attr = args.shift
559 560 text = obj.send(attr).to_s
560 561 else
561 562 raise ArgumentError, 'invalid arguments to textilizable'
562 563 end
563 564 return '' if text.blank?
564 565 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
565 566 only_path = options.delete(:only_path) == false ? false : true
566 567
567 568 text = text.dup
568 569 macros = catch_macros(text)
569 570 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
570 571
571 572 @parsed_headings = []
572 573 @heading_anchors = {}
573 574 @current_section = 0 if options[:edit_section_links]
574 575
575 576 parse_sections(text, project, obj, attr, only_path, options)
576 577 text = parse_non_pre_blocks(text, obj, macros) do |text|
577 578 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
578 579 send method_name, text, project, obj, attr, only_path, options
579 580 end
580 581 end
581 582 parse_headings(text, project, obj, attr, only_path, options)
582 583
583 584 if @parsed_headings.any?
584 585 replace_toc(text, @parsed_headings)
585 586 end
586 587
587 588 text.html_safe
588 589 end
589 590
590 591 def parse_non_pre_blocks(text, obj, macros)
591 592 s = StringScanner.new(text)
592 593 tags = []
593 594 parsed = ''
594 595 while !s.eos?
595 596 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
596 597 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
597 598 if tags.empty?
598 599 yield text
599 600 inject_macros(text, obj, macros) if macros.any?
600 601 else
601 602 inject_macros(text, obj, macros, false) if macros.any?
602 603 end
603 604 parsed << text
604 605 if tag
605 606 if closing
606 607 if tags.last == tag.downcase
607 608 tags.pop
608 609 end
609 610 else
610 611 tags << tag.downcase
611 612 end
612 613 parsed << full_tag
613 614 end
614 615 end
615 616 # Close any non closing tags
616 617 while tag = tags.pop
617 618 parsed << "</#{tag}>"
618 619 end
619 620 parsed
620 621 end
621 622
622 623 def parse_inline_attachments(text, project, obj, attr, only_path, options)
623 624 # when using an image link, try to use an attachment, if possible
624 625 attachments = options[:attachments] || []
625 626 attachments += obj.attachments if obj.respond_to?(:attachments)
626 627 if attachments.present?
627 628 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
628 629 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
629 630 # search for the picture in attachments
630 631 if found = Attachment.latest_attach(attachments, filename)
631 632 image_url = download_named_attachment_path(found, found.filename, :only_path => only_path)
632 633 desc = found.description.to_s.gsub('"', '')
633 634 if !desc.blank? && alttext.blank?
634 635 alt = " title=\"#{desc}\" alt=\"#{desc}\""
635 636 end
636 637 "src=\"#{image_url}\"#{alt}"
637 638 else
638 639 m
639 640 end
640 641 end
641 642 end
642 643 end
643 644
644 645 # Wiki links
645 646 #
646 647 # Examples:
647 648 # [[mypage]]
648 649 # [[mypage|mytext]]
649 650 # wiki links can refer other project wikis, using project name or identifier:
650 651 # [[project:]] -> wiki starting page
651 652 # [[project:|mytext]]
652 653 # [[project:mypage]]
653 654 # [[project:mypage|mytext]]
654 655 def parse_wiki_links(text, project, obj, attr, only_path, options)
655 656 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
656 657 link_project = project
657 658 esc, all, page, title = $1, $2, $3, $5
658 659 if esc.nil?
659 660 if page =~ /^([^\:]+)\:(.*)$/
660 661 identifier, page = $1, $2
661 662 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
662 663 title ||= identifier if page.blank?
663 664 end
664 665
665 666 if link_project && link_project.wiki
666 667 # extract anchor
667 668 anchor = nil
668 669 if page =~ /^(.+?)\#(.+)$/
669 670 page, anchor = $1, $2
670 671 end
671 672 anchor = sanitize_anchor_name(anchor) if anchor.present?
672 673 # check if page exists
673 674 wiki_page = link_project.wiki.find_page(page)
674 675 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
675 676 "##{anchor}"
676 677 else
677 678 case options[:wiki_links]
678 679 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
679 680 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
680 681 else
681 682 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
682 683 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
683 684 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
684 685 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
685 686 end
686 687 end
687 688 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
688 689 else
689 690 # project or wiki doesn't exist
690 691 all
691 692 end
692 693 else
693 694 all
694 695 end
695 696 end
696 697 end
697 698
698 699 # Redmine links
699 700 #
700 701 # Examples:
701 702 # Issues:
702 703 # #52 -> Link to issue #52
703 704 # Changesets:
704 705 # r52 -> Link to revision 52
705 706 # commit:a85130f -> Link to scmid starting with a85130f
706 707 # Documents:
707 708 # document#17 -> Link to document with id 17
708 709 # document:Greetings -> Link to the document with title "Greetings"
709 710 # document:"Some document" -> Link to the document with title "Some document"
710 711 # Versions:
711 712 # version#3 -> Link to version with id 3
712 713 # version:1.0.0 -> Link to version named "1.0.0"
713 714 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
714 715 # Attachments:
715 716 # attachment:file.zip -> Link to the attachment of the current object named file.zip
716 717 # Source files:
717 718 # source:some/file -> Link to the file located at /some/file in the project's repository
718 719 # source:some/file@52 -> Link to the file's revision 52
719 720 # source:some/file#L120 -> Link to line 120 of the file
720 721 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
721 722 # export:some/file -> Force the download of the file
722 723 # Forum messages:
723 724 # message#1218 -> Link to message with id 1218
724 725 # Projects:
725 726 # project:someproject -> Link to project named "someproject"
726 727 # project#3 -> Link to project with id 3
727 728 #
728 729 # Links can refer other objects from other projects, using project identifier:
729 730 # identifier:r52
730 731 # identifier:document:"Some document"
731 732 # identifier:version:1.0.0
732 733 # identifier:source:some/file
733 734 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
734 735 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|
735 736 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
736 737 link = nil
737 738 project = default_project
738 739 if project_identifier
739 740 project = Project.visible.find_by_identifier(project_identifier)
740 741 end
741 742 if esc.nil?
742 743 if prefix.nil? && sep == 'r'
743 744 if project
744 745 repository = nil
745 746 if repo_identifier
746 747 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
747 748 else
748 749 repository = project.repository
749 750 end
750 751 # project.changesets.visible raises an SQL error because of a double join on repositories
751 752 if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier))
752 753 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},
753 754 :class => 'changeset',
754 755 :title => truncate_single_line(changeset.comments, :length => 100))
755 756 end
756 757 end
757 758 elsif sep == '#'
758 759 oid = identifier.to_i
759 760 case prefix
760 761 when nil
761 762 if oid.to_s == identifier && issue = Issue.visible.find_by_id(oid, :include => :status)
762 763 anchor = comment_id ? "note-#{comment_id}" : nil
763 764 link = link_to(h("##{oid}#{comment_suffix}"), {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor},
764 765 :class => issue.css_classes,
765 766 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
766 767 end
767 768 when 'document'
768 769 if document = Document.visible.find_by_id(oid)
769 770 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
770 771 :class => 'document'
771 772 end
772 773 when 'version'
773 774 if version = Version.visible.find_by_id(oid)
774 775 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
775 776 :class => 'version'
776 777 end
777 778 when 'message'
778 779 if message = Message.visible.find_by_id(oid, :include => :parent)
779 780 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
780 781 end
781 782 when 'forum'
782 783 if board = Board.visible.find_by_id(oid)
783 784 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
784 785 :class => 'board'
785 786 end
786 787 when 'news'
787 788 if news = News.visible.find_by_id(oid)
788 789 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
789 790 :class => 'news'
790 791 end
791 792 when 'project'
792 793 if p = Project.visible.find_by_id(oid)
793 794 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
794 795 end
795 796 end
796 797 elsif sep == ':'
797 798 # removes the double quotes if any
798 799 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
799 800 case prefix
800 801 when 'document'
801 802 if project && document = project.documents.visible.find_by_title(name)
802 803 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
803 804 :class => 'document'
804 805 end
805 806 when 'version'
806 807 if project && version = project.versions.visible.find_by_name(name)
807 808 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
808 809 :class => 'version'
809 810 end
810 811 when 'forum'
811 812 if project && board = project.boards.visible.find_by_name(name)
812 813 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
813 814 :class => 'board'
814 815 end
815 816 when 'news'
816 817 if project && news = project.news.visible.find_by_title(name)
817 818 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
818 819 :class => 'news'
819 820 end
820 821 when 'commit', 'source', 'export'
821 822 if project
822 823 repository = nil
823 824 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
824 825 repo_prefix, repo_identifier, name = $1, $2, $3
825 826 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
826 827 else
827 828 repository = project.repository
828 829 end
829 830 if prefix == 'commit'
830 831 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
831 832 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},
832 833 :class => 'changeset',
833 834 :title => truncate_single_line(changeset.comments, :length => 100)
834 835 end
835 836 else
836 837 if repository && User.current.allowed_to?(:browse_repository, project)
837 838 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
838 839 path, rev, anchor = $1, $3, $5
839 840 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
840 841 :path => to_path_param(path),
841 842 :rev => rev,
842 843 :anchor => anchor},
843 844 :class => (prefix == 'export' ? 'source download' : 'source')
844 845 end
845 846 end
846 847 repo_prefix = nil
847 848 end
848 849 when 'attachment'
849 850 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
850 851 if attachments && attachment = Attachment.latest_attach(attachments, name)
851 852 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
852 853 end
853 854 when 'project'
854 855 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
855 856 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
856 857 end
857 858 end
858 859 end
859 860 end
860 861 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
861 862 end
862 863 end
863 864
864 865 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
865 866
866 867 def parse_sections(text, project, obj, attr, only_path, options)
867 868 return unless options[:edit_section_links]
868 869 text.gsub!(HEADING_RE) do
869 870 heading = $1
870 871 @current_section += 1
871 872 if @current_section > 1
872 873 content_tag('div',
873 874 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
874 875 :class => 'contextual',
875 876 :title => l(:button_edit_section),
876 877 :id => "section-#{@current_section}") + heading.html_safe
877 878 else
878 879 heading
879 880 end
880 881 end
881 882 end
882 883
883 884 # Headings and TOC
884 885 # Adds ids and links to headings unless options[:headings] is set to false
885 886 def parse_headings(text, project, obj, attr, only_path, options)
886 887 return if options[:headings] == false
887 888
888 889 text.gsub!(HEADING_RE) do
889 890 level, attrs, content = $2.to_i, $3, $4
890 891 item = strip_tags(content).strip
891 892 anchor = sanitize_anchor_name(item)
892 893 # used for single-file wiki export
893 894 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
894 895 @heading_anchors[anchor] ||= 0
895 896 idx = (@heading_anchors[anchor] += 1)
896 897 if idx > 1
897 898 anchor = "#{anchor}-#{idx}"
898 899 end
899 900 @parsed_headings << [level, anchor, item]
900 901 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
901 902 end
902 903 end
903 904
904 905 MACROS_RE = /(
905 906 (!)? # escaping
906 907 (
907 908 \{\{ # opening tag
908 909 ([\w]+) # macro name
909 910 (\(([^\n\r]*?)\))? # optional arguments
910 911 ([\n\r].*?[\n\r])? # optional block of text
911 912 \}\} # closing tag
912 913 )
913 914 )/mx unless const_defined?(:MACROS_RE)
914 915
915 916 MACRO_SUB_RE = /(
916 917 \{\{
917 918 macro\((\d+)\)
918 919 \}\}
919 920 )/x unless const_defined?(:MACRO_SUB_RE)
920 921
921 922 # Extracts macros from text
922 923 def catch_macros(text)
923 924 macros = {}
924 925 text.gsub!(MACROS_RE) do
925 926 all, macro = $1, $4.downcase
926 927 if macro_exists?(macro) || all =~ MACRO_SUB_RE
927 928 index = macros.size
928 929 macros[index] = all
929 930 "{{macro(#{index})}}"
930 931 else
931 932 all
932 933 end
933 934 end
934 935 macros
935 936 end
936 937
937 938 # Executes and replaces macros in text
938 939 def inject_macros(text, obj, macros, execute=true)
939 940 text.gsub!(MACRO_SUB_RE) do
940 941 all, index = $1, $2.to_i
941 942 orig = macros.delete(index)
942 943 if execute && orig && orig =~ MACROS_RE
943 944 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
944 945 if esc.nil?
945 946 h(exec_macro(macro, obj, args, block) || all)
946 947 else
947 948 h(all)
948 949 end
949 950 elsif orig
950 951 h(orig)
951 952 else
952 953 h(all)
953 954 end
954 955 end
955 956 end
956 957
957 958 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
958 959
959 960 # Renders the TOC with given headings
960 961 def replace_toc(text, headings)
961 962 text.gsub!(TOC_RE) do
962 963 # Keep only the 4 first levels
963 964 headings = headings.select{|level, anchor, item| level <= 4}
964 965 if headings.empty?
965 966 ''
966 967 else
967 968 div_class = 'toc'
968 969 div_class << ' right' if $1 == '>'
969 970 div_class << ' left' if $1 == '<'
970 971 out = "<ul class=\"#{div_class}\"><li>"
971 972 root = headings.map(&:first).min
972 973 current = root
973 974 started = false
974 975 headings.each do |level, anchor, item|
975 976 if level > current
976 977 out << '<ul><li>' * (level - current)
977 978 elsif level < current
978 979 out << "</li></ul>\n" * (current - level) + "</li><li>"
979 980 elsif started
980 981 out << '</li><li>'
981 982 end
982 983 out << "<a href=\"##{anchor}\">#{item}</a>"
983 984 current = level
984 985 started = true
985 986 end
986 987 out << '</li></ul>' * (current - root)
987 988 out << '</li></ul>'
988 989 end
989 990 end
990 991 end
991 992
992 993 # Same as Rails' simple_format helper without using paragraphs
993 994 def simple_format_without_paragraph(text)
994 995 text.to_s.
995 996 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
996 997 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
997 998 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
998 999 html_safe
999 1000 end
1000 1001
1001 1002 def lang_options_for_select(blank=true)
1002 1003 (blank ? [["(auto)", ""]] : []) + languages_options
1003 1004 end
1004 1005
1005 1006 def label_tag_for(name, option_tags = nil, options = {})
1006 1007 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
1007 1008 content_tag("label", label_text)
1008 1009 end
1009 1010
1010 1011 def labelled_form_for(*args, &proc)
1011 1012 args << {} unless args.last.is_a?(Hash)
1012 1013 options = args.last
1013 1014 if args.first.is_a?(Symbol)
1014 1015 options.merge!(:as => args.shift)
1015 1016 end
1016 1017 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1017 1018 form_for(*args, &proc)
1018 1019 end
1019 1020
1020 1021 def labelled_fields_for(*args, &proc)
1021 1022 args << {} unless args.last.is_a?(Hash)
1022 1023 options = args.last
1023 1024 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1024 1025 fields_for(*args, &proc)
1025 1026 end
1026 1027
1027 1028 def labelled_remote_form_for(*args, &proc)
1028 1029 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_remote_form_for is deprecated and will be removed in Redmine 2.2."
1029 1030 args << {} unless args.last.is_a?(Hash)
1030 1031 options = args.last
1031 1032 options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
1032 1033 form_for(*args, &proc)
1033 1034 end
1034 1035
1035 1036 def error_messages_for(*objects)
1036 1037 html = ""
1037 1038 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1038 1039 errors = objects.map {|o| o.errors.full_messages}.flatten
1039 1040 if errors.any?
1040 1041 html << "<div id='errorExplanation'><ul>\n"
1041 1042 errors.each do |error|
1042 1043 html << "<li>#{h error}</li>\n"
1043 1044 end
1044 1045 html << "</ul></div>\n"
1045 1046 end
1046 1047 html.html_safe
1047 1048 end
1048 1049
1049 1050 def delete_link(url, options={})
1050 1051 options = {
1051 1052 :method => :delete,
1052 1053 :data => {:confirm => l(:text_are_you_sure)},
1053 1054 :class => 'icon icon-del'
1054 1055 }.merge(options)
1055 1056
1056 1057 link_to l(:button_delete), url, options
1057 1058 end
1058 1059
1059 1060 def preview_link(url, form, target='preview', options={})
1060 1061 content_tag 'a', l(:label_preview), {
1061 1062 :href => "#",
1062 1063 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1063 1064 :accesskey => accesskey(:preview)
1064 1065 }.merge(options)
1065 1066 end
1066 1067
1067 1068 def link_to_function(name, function, html_options={})
1068 1069 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1069 1070 end
1070 1071
1071 1072 # Helper to render JSON in views
1072 1073 def raw_json(arg)
1073 1074 arg.to_json.to_s.gsub('/', '\/').html_safe
1074 1075 end
1075 1076
1076 1077 def back_url
1077 1078 url = params[:back_url]
1078 1079 if url.nil? && referer = request.env['HTTP_REFERER']
1079 1080 url = CGI.unescape(referer.to_s)
1080 1081 end
1081 1082 url
1082 1083 end
1083 1084
1084 1085 def back_url_hidden_field_tag
1085 1086 url = back_url
1086 1087 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1087 1088 end
1088 1089
1089 1090 def check_all_links(form_name)
1090 1091 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1091 1092 " | ".html_safe +
1092 1093 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1093 1094 end
1094 1095
1095 1096 def progress_bar(pcts, options={})
1096 1097 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1097 1098 pcts = pcts.collect(&:round)
1098 1099 pcts[1] = pcts[1] - pcts[0]
1099 1100 pcts << (100 - pcts[1] - pcts[0])
1100 1101 width = options[:width] || '100px;'
1101 1102 legend = options[:legend] || ''
1102 1103 content_tag('table',
1103 1104 content_tag('tr',
1104 1105 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1105 1106 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1106 1107 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1107 1108 ), :class => "progress progress-#{pcts[0]}", :style => "width: #{width};").html_safe +
1108 1109 content_tag('p', legend, :class => 'percent').html_safe
1109 1110 end
1110 1111
1111 1112 def checked_image(checked=true)
1112 1113 if checked
1113 1114 image_tag 'toggle_check.png'
1114 1115 end
1115 1116 end
1116 1117
1117 1118 def context_menu(url)
1118 1119 unless @context_menu_included
1119 1120 content_for :header_tags do
1120 1121 javascript_include_tag('context_menu') +
1121 1122 stylesheet_link_tag('context_menu')
1122 1123 end
1123 1124 if l(:direction) == 'rtl'
1124 1125 content_for :header_tags do
1125 1126 stylesheet_link_tag('context_menu_rtl')
1126 1127 end
1127 1128 end
1128 1129 @context_menu_included = true
1129 1130 end
1130 1131 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1131 1132 end
1132 1133
1133 1134 def calendar_for(field_id)
1134 1135 include_calendar_headers_tags
1135 1136 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1136 1137 end
1137 1138
1138 1139 def include_calendar_headers_tags
1139 1140 unless @calendar_headers_tags_included
1140 1141 tags = javascript_include_tag("datepicker")
1141 1142 @calendar_headers_tags_included = true
1142 1143 content_for :header_tags do
1143 1144 start_of_week = Setting.start_of_week
1144 1145 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1145 1146 # Redmine uses 1..7 (monday..sunday) in settings and locales
1146 1147 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1147 1148 start_of_week = start_of_week.to_i % 7
1148 1149 tags << javascript_tag(
1149 1150 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1150 1151 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1151 1152 path_to_image('/images/calendar.png') +
1152 1153 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1153 1154 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1154 1155 "beforeShow: beforeShowDatePicker};")
1155 1156 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1156 1157 unless jquery_locale == 'en'
1157 1158 tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js")
1158 1159 end
1159 1160 tags
1160 1161 end
1161 1162 end
1162 1163 end
1163 1164
1164 1165 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1165 1166 # Examples:
1166 1167 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1167 1168 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1168 1169 #
1169 1170 def stylesheet_link_tag(*sources)
1170 1171 options = sources.last.is_a?(Hash) ? sources.pop : {}
1171 1172 plugin = options.delete(:plugin)
1172 1173 sources = sources.map do |source|
1173 1174 if plugin
1174 1175 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1175 1176 elsif current_theme && current_theme.stylesheets.include?(source)
1176 1177 current_theme.stylesheet_path(source)
1177 1178 else
1178 1179 source
1179 1180 end
1180 1181 end
1181 1182 super sources, options
1182 1183 end
1183 1184
1184 1185 # Overrides Rails' image_tag with themes and plugins support.
1185 1186 # Examples:
1186 1187 # image_tag('image.png') # => picks image.png from the current theme or defaults
1187 1188 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1188 1189 #
1189 1190 def image_tag(source, options={})
1190 1191 if plugin = options.delete(:plugin)
1191 1192 source = "/plugin_assets/#{plugin}/images/#{source}"
1192 1193 elsif current_theme && current_theme.images.include?(source)
1193 1194 source = current_theme.image_path(source)
1194 1195 end
1195 1196 super source, options
1196 1197 end
1197 1198
1198 1199 # Overrides Rails' javascript_include_tag with plugins support
1199 1200 # Examples:
1200 1201 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1201 1202 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1202 1203 #
1203 1204 def javascript_include_tag(*sources)
1204 1205 options = sources.last.is_a?(Hash) ? sources.pop : {}
1205 1206 if plugin = options.delete(:plugin)
1206 1207 sources = sources.map do |source|
1207 1208 if plugin
1208 1209 "/plugin_assets/#{plugin}/javascripts/#{source}"
1209 1210 else
1210 1211 source
1211 1212 end
1212 1213 end
1213 1214 end
1214 1215 super sources, options
1215 1216 end
1216 1217
1217 1218 # TODO: remove this in 2.5.0
1218 1219 def has_content?(name)
1219 1220 content_for?(name)
1220 1221 end
1221 1222
1222 1223 def sidebar_content?
1223 1224 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1224 1225 end
1225 1226
1226 1227 def view_layouts_base_sidebar_hook_response
1227 1228 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1228 1229 end
1229 1230
1230 1231 def email_delivery_enabled?
1231 1232 !!ActionMailer::Base.perform_deliveries
1232 1233 end
1233 1234
1234 1235 # Returns the avatar image tag for the given +user+ if avatars are enabled
1235 1236 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1236 1237 def avatar(user, options = { })
1237 1238 if Setting.gravatar_enabled?
1238 1239 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1239 1240 email = nil
1240 1241 if user.respond_to?(:mail)
1241 1242 email = user.mail
1242 1243 elsif user.to_s =~ %r{<(.+?)>}
1243 1244 email = $1
1244 1245 end
1245 1246 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1246 1247 else
1247 1248 ''
1248 1249 end
1249 1250 end
1250 1251
1251 1252 def sanitize_anchor_name(anchor)
1252 1253 if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java'
1253 1254 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1254 1255 else
1255 1256 # TODO: remove when ruby1.8 is no longer supported
1256 1257 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1257 1258 end
1258 1259 end
1259 1260
1260 1261 # Returns the javascript tags that are included in the html layout head
1261 1262 def javascript_heads
1262 1263 tags = javascript_include_tag('jquery-1.8.3-ui-1.9.2-ujs-2.0.3', 'application')
1263 1264 unless User.current.pref.warn_on_leaving_unsaved == '0'
1264 1265 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1265 1266 end
1266 1267 tags
1267 1268 end
1268 1269
1269 1270 def favicon
1270 1271 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1271 1272 end
1272 1273
1273 1274 def robot_exclusion_tag
1274 1275 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1275 1276 end
1276 1277
1277 1278 # Returns true if arg is expected in the API response
1278 1279 def include_in_api_response?(arg)
1279 1280 unless @included_in_api_response
1280 1281 param = params[:include]
1281 1282 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1282 1283 @included_in_api_response.collect!(&:strip)
1283 1284 end
1284 1285 @included_in_api_response.include?(arg.to_s)
1285 1286 end
1286 1287
1287 1288 # Returns options or nil if nometa param or X-Redmine-Nometa header
1288 1289 # was set in the request
1289 1290 def api_meta(options)
1290 1291 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1291 1292 # compatibility mode for activeresource clients that raise
1292 1293 # an error when unserializing an array with attributes
1293 1294 nil
1294 1295 else
1295 1296 options
1296 1297 end
1297 1298 end
1298 1299
1299 1300 private
1300 1301
1301 1302 def wiki_helper
1302 1303 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1303 1304 extend helper
1304 1305 return self
1305 1306 end
1306 1307
1307 1308 def link_to_content_update(text, url_params = {}, html_options = {})
1308 1309 link_to(text, url_params, html_options)
1309 1310 end
1310 1311 end
@@ -1,429 +1,429
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 module IssuesHelper
21 21 include ApplicationHelper
22 22
23 23 def issue_list(issues, &block)
24 24 ancestors = []
25 25 issues.each do |issue|
26 26 while (ancestors.any? && !issue.is_descendant_of?(ancestors.last))
27 27 ancestors.pop
28 28 end
29 29 yield issue, ancestors.size
30 30 ancestors << issue unless issue.leaf?
31 31 end
32 32 end
33 33
34 34 # Renders a HTML/CSS tooltip
35 35 #
36 36 # To use, a trigger div is needed. This is a div with the class of "tooltip"
37 37 # that contains this method wrapped in a span with the class of "tip"
38 38 #
39 39 # <div class="tooltip"><%= link_to_issue(issue) %>
40 40 # <span class="tip"><%= render_issue_tooltip(issue) %></span>
41 41 # </div>
42 42 #
43 43 def render_issue_tooltip(issue)
44 44 @cached_label_status ||= l(:field_status)
45 45 @cached_label_start_date ||= l(:field_start_date)
46 46 @cached_label_due_date ||= l(:field_due_date)
47 47 @cached_label_assigned_to ||= l(:field_assigned_to)
48 48 @cached_label_priority ||= l(:field_priority)
49 49 @cached_label_project ||= l(:field_project)
50 50
51 51 link_to_issue(issue) + "<br /><br />".html_safe +
52 52 "<strong>#{@cached_label_project}</strong>: #{link_to_project(issue.project)}<br />".html_safe +
53 53 "<strong>#{@cached_label_status}</strong>: #{h(issue.status.name)}<br />".html_safe +
54 54 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />".html_safe +
55 55 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />".html_safe +
56 56 "<strong>#{@cached_label_assigned_to}</strong>: #{h(issue.assigned_to)}<br />".html_safe +
57 57 "<strong>#{@cached_label_priority}</strong>: #{h(issue.priority.name)}".html_safe
58 58 end
59 59
60 60 def issue_heading(issue)
61 61 h("#{issue.tracker} ##{issue.id}")
62 62 end
63 63
64 64 def render_issue_subject_with_tree(issue)
65 65 s = ''
66 66 ancestors = issue.root? ? [] : issue.ancestors.visible.all
67 67 ancestors.each do |ancestor|
68 68 s << '<div>' + content_tag('p', link_to_issue(ancestor, :project => (issue.project_id != ancestor.project_id)))
69 69 end
70 70 s << '<div>'
71 71 subject = h(issue.subject)
72 72 if issue.is_private?
73 73 subject = content_tag('span', l(:field_is_private), :class => 'private') + ' ' + subject
74 74 end
75 75 s << content_tag('h3', subject)
76 76 s << '</div>' * (ancestors.size + 1)
77 77 s.html_safe
78 78 end
79 79
80 80 def render_descendants_tree(issue)
81 81 s = '<form><table class="list issues">'
82 82 issue_list(issue.descendants.visible.sort_by(&:lft)) do |child, level|
83 83 css = "issue issue-#{child.id} hascontextmenu"
84 84 css << " idnt idnt-#{level}" if level > 0
85 85 s << content_tag('tr',
86 86 content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') +
87 87 content_tag('td', link_to_issue(child, :truncate => 60, :project => (issue.project_id != child.project_id)), :class => 'subject') +
88 88 content_tag('td', h(child.status)) +
89 89 content_tag('td', link_to_user(child.assigned_to)) +
90 90 content_tag('td', progress_bar(child.done_ratio, :width => '80px')),
91 91 :class => css)
92 92 end
93 93 s << '</table></form>'
94 94 s.html_safe
95 95 end
96 96
97 97 # Returns an array of error messages for bulk edited issues
98 98 def bulk_edit_error_messages(issues)
99 99 messages = {}
100 100 issues.each do |issue|
101 101 issue.errors.full_messages.each do |message|
102 102 messages[message] ||= []
103 103 messages[message] << issue
104 104 end
105 105 end
106 106 messages.map { |message, issues|
107 107 "#{message}: " + issues.map {|i| "##{i.id}"}.join(', ')
108 108 }
109 109 end
110 110
111 111 # Returns a link for adding a new subtask to the given issue
112 112 def link_to_new_subtask(issue)
113 113 attrs = {
114 114 :tracker_id => issue.tracker,
115 115 :parent_issue_id => issue
116 116 }
117 117 link_to(l(:button_add), new_project_issue_path(issue.project, :issue => attrs))
118 118 end
119 119
120 120 class IssueFieldsRows
121 121 include ActionView::Helpers::TagHelper
122 122
123 123 def initialize
124 124 @left = []
125 125 @right = []
126 126 end
127 127
128 128 def left(*args)
129 129 args.any? ? @left << cells(*args) : @left
130 130 end
131 131
132 132 def right(*args)
133 133 args.any? ? @right << cells(*args) : @right
134 134 end
135 135
136 136 def size
137 137 @left.size > @right.size ? @left.size : @right.size
138 138 end
139 139
140 140 def to_html
141 141 html = ''.html_safe
142 142 blank = content_tag('th', '') + content_tag('td', '')
143 143 size.times do |i|
144 144 left = @left[i] || blank
145 145 right = @right[i] || blank
146 146 html << content_tag('tr', left + right)
147 147 end
148 148 html
149 149 end
150 150
151 151 def cells(label, text, options={})
152 152 content_tag('th', "#{label}:", options) + content_tag('td', text, options)
153 153 end
154 154 end
155 155
156 156 def issue_fields_rows
157 157 r = IssueFieldsRows.new
158 158 yield r
159 159 r.to_html
160 160 end
161 161
162 162 def render_custom_fields_rows(issue)
163 163 values = issue.visible_custom_field_values
164 164 return if values.empty?
165 165 ordered_values = []
166 166 half = (values.size / 2.0).ceil
167 167 half.times do |i|
168 168 ordered_values << values[i]
169 169 ordered_values << values[i + half]
170 170 end
171 171 s = "<tr>\n"
172 172 n = 0
173 173 ordered_values.compact.each do |value|
174 174 css = "cf_#{value.custom_field.id}"
175 175 s << "</tr>\n<tr>\n" if n > 0 && (n % 2) == 0
176 176 s << "\t<th class=\"#{css}\">#{ h(value.custom_field.name) }:</th><td class=\"#{css}\">#{ h(show_value(value)) }</td>\n"
177 177 n += 1
178 178 end
179 179 s << "</tr>\n"
180 180 s.html_safe
181 181 end
182 182
183 183 def issues_destroy_confirmation_message(issues)
184 184 issues = [issues] unless issues.is_a?(Array)
185 185 message = l(:text_issues_destroy_confirmation)
186 186 descendant_count = issues.inject(0) {|memo, i| memo += (i.right - i.left - 1)/2}
187 187 if descendant_count > 0
188 188 issues.each do |issue|
189 189 next if issue.root?
190 190 issues.each do |other_issue|
191 191 descendant_count -= 1 if issue.is_descendant_of?(other_issue)
192 192 end
193 193 end
194 194 if descendant_count > 0
195 195 message << "\n" + l(:text_issues_destroy_descendants_confirmation, :count => descendant_count)
196 196 end
197 197 end
198 198 message
199 199 end
200 200
201 201 def sidebar_queries
202 202 unless @sidebar_queries
203 203 @sidebar_queries = IssueQuery.visible.
204 204 order("#{Query.table_name}.name ASC").
205 205 # Project specific queries and global queries
206 206 where(@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id]).
207 207 all
208 208 end
209 209 @sidebar_queries
210 210 end
211 211
212 212 def query_links(title, queries)
213 213 return '' if queries.empty?
214 214 # links to #index on issues/show
215 215 url_params = controller_name == 'issues' ? {:controller => 'issues', :action => 'index', :project_id => @project} : params
216 216
217 217 content_tag('h3', title) + "\n" +
218 218 content_tag('ul',
219 219 queries.collect {|query|
220 220 css = 'query'
221 221 css << ' selected' if query == @query
222 222 content_tag('li', link_to(query.name, url_params.merge(:query_id => query), :class => css))
223 223 }.join("\n").html_safe,
224 224 :class => 'queries'
225 225 ) + "\n"
226 226 end
227 227
228 228 def render_sidebar_queries
229 229 out = ''.html_safe
230 230 out << query_links(l(:label_my_queries), sidebar_queries.select(&:is_private?))
231 231 out << query_links(l(:label_query_plural), sidebar_queries.reject(&:is_private?))
232 232 out
233 233 end
234 234
235 235 def email_issue_attributes(issue, user)
236 236 items = []
237 237 %w(author status priority assigned_to category fixed_version).each do |attribute|
238 238 unless issue.disabled_core_fields.include?(attribute+"_id")
239 239 items << "#{l("field_#{attribute}")}: #{issue.send attribute}"
240 240 end
241 241 end
242 242 issue.visible_custom_field_values(user).each do |value|
243 243 items << "#{value.custom_field.name}: #{show_value(value, false)}"
244 244 end
245 245 items
246 246 end
247 247
248 248 def render_email_issue_attributes(issue, user, html=false)
249 249 items = email_issue_attributes(issue, user)
250 250 if html
251 251 content_tag('ul', items.map{|s| content_tag('li', s)}.join("\n").html_safe)
252 252 else
253 253 items.map{|s| "* #{s}"}.join("\n")
254 254 end
255 255 end
256 256
257 257 # Returns the textual representation of a journal details
258 258 # as an array of strings
259 259 def details_to_strings(details, no_html=false, options={})
260 260 options[:only_path] = (options[:only_path] == false ? false : true)
261 261 strings = []
262 262 values_by_field = {}
263 263 details.each do |detail|
264 264 if detail.property == 'cf'
265 265 field = detail.custom_field
266 266 if field && field.multiple?
267 267 values_by_field[field] ||= {:added => [], :deleted => []}
268 268 if detail.old_value
269 269 values_by_field[field][:deleted] << detail.old_value
270 270 end
271 271 if detail.value
272 272 values_by_field[field][:added] << detail.value
273 273 end
274 274 next
275 275 end
276 276 end
277 277 strings << show_detail(detail, no_html, options)
278 278 end
279 279 values_by_field.each do |field, changes|
280 280 detail = JournalDetail.new(:property => 'cf', :prop_key => field.id.to_s)
281 281 detail.instance_variable_set "@custom_field", field
282 282 if changes[:added].any?
283 283 detail.value = changes[:added]
284 284 strings << show_detail(detail, no_html, options)
285 285 elsif changes[:deleted].any?
286 286 detail.old_value = changes[:deleted]
287 287 strings << show_detail(detail, no_html, options)
288 288 end
289 289 end
290 290 strings
291 291 end
292 292
293 293 # Returns the textual representation of a single journal detail
294 294 def show_detail(detail, no_html=false, options={})
295 295 multiple = false
296 296 case detail.property
297 297 when 'attr'
298 298 field = detail.prop_key.to_s.gsub(/\_id$/, "")
299 299 label = l(("field_" + field).to_sym)
300 300 case detail.prop_key
301 301 when 'due_date', 'start_date'
302 302 value = format_date(detail.value.to_date) if detail.value
303 303 old_value = format_date(detail.old_value.to_date) if detail.old_value
304 304
305 305 when 'project_id', 'status_id', 'tracker_id', 'assigned_to_id',
306 306 'priority_id', 'category_id', 'fixed_version_id'
307 307 value = find_name_by_reflection(field, detail.value)
308 308 old_value = find_name_by_reflection(field, detail.old_value)
309 309
310 310 when 'estimated_hours'
311 311 value = "%0.02f" % detail.value.to_f unless detail.value.blank?
312 312 old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank?
313 313
314 314 when 'parent_id'
315 315 label = l(:field_parent_issue)
316 316 value = "##{detail.value}" unless detail.value.blank?
317 317 old_value = "##{detail.old_value}" unless detail.old_value.blank?
318 318
319 319 when 'is_private'
320 320 value = l(detail.value == "0" ? :general_text_No : :general_text_Yes) unless detail.value.blank?
321 321 old_value = l(detail.old_value == "0" ? :general_text_No : :general_text_Yes) unless detail.old_value.blank?
322 322 end
323 323 when 'cf'
324 324 custom_field = detail.custom_field
325 325 if custom_field
326 326 multiple = custom_field.multiple?
327 327 label = custom_field.name
328 328 value = format_value(detail.value, custom_field) if detail.value
329 329 old_value = format_value(detail.old_value, custom_field) if detail.old_value
330 330 end
331 331 when 'attachment'
332 332 label = l(:label_attachment)
333 333 when 'relation'
334 334 if detail.value && !detail.old_value
335 335 rel_issue = Issue.visible.find_by_id(detail.value)
336 336 value = rel_issue.nil? ? "#{l(:label_issue)} ##{detail.value}" :
337 (no_html ? rel_issue : link_to_issue(rel_issue))
337 (no_html ? rel_issue : link_to_issue(rel_issue, :only_path => options[:only_path]))
338 338 elsif detail.old_value && !detail.value
339 339 rel_issue = Issue.visible.find_by_id(detail.old_value)
340 340 old_value = rel_issue.nil? ? "#{l(:label_issue)} ##{detail.old_value}" :
341 (no_html ? rel_issue : link_to_issue(rel_issue))
341 (no_html ? rel_issue : link_to_issue(rel_issue, :only_path => options[:only_path]))
342 342 end
343 343 label = l(detail.prop_key.to_sym)
344 344 end
345 345 call_hook(:helper_issues_show_detail_after_setting,
346 346 {:detail => detail, :label => label, :value => value, :old_value => old_value })
347 347
348 348 label ||= detail.prop_key
349 349 value ||= detail.value
350 350 old_value ||= detail.old_value
351 351
352 352 unless no_html
353 353 label = content_tag('strong', label)
354 354 old_value = content_tag("i", h(old_value)) if detail.old_value
355 355 if detail.old_value && detail.value.blank? && detail.property != 'relation'
356 356 old_value = content_tag("del", old_value)
357 357 end
358 358 if detail.property == 'attachment' && !value.blank? && atta = Attachment.find_by_id(detail.prop_key)
359 359 # Link to the attachment if it has not been removed
360 360 value = link_to_attachment(atta, :download => true, :only_path => options[:only_path])
361 361 if options[:only_path] != false && atta.is_text?
362 362 value += link_to(
363 363 image_tag('magnifier.png'),
364 364 :controller => 'attachments', :action => 'show',
365 365 :id => atta, :filename => atta.filename
366 366 )
367 367 end
368 368 else
369 369 value = content_tag("i", h(value)) if value
370 370 end
371 371 end
372 372
373 373 if detail.property == 'attr' && detail.prop_key == 'description'
374 374 s = l(:text_journal_changed_no_detail, :label => label)
375 375 unless no_html
376 376 diff_link = link_to 'diff',
377 377 {:controller => 'journals', :action => 'diff', :id => detail.journal_id,
378 378 :detail_id => detail.id, :only_path => options[:only_path]},
379 379 :title => l(:label_view_diff)
380 380 s << " (#{ diff_link })"
381 381 end
382 382 s.html_safe
383 383 elsif detail.value.present?
384 384 case detail.property
385 385 when 'attr', 'cf'
386 386 if detail.old_value.present?
387 387 l(:text_journal_changed, :label => label, :old => old_value, :new => value).html_safe
388 388 elsif multiple
389 389 l(:text_journal_added, :label => label, :value => value).html_safe
390 390 else
391 391 l(:text_journal_set_to, :label => label, :value => value).html_safe
392 392 end
393 393 when 'attachment', 'relation'
394 394 l(:text_journal_added, :label => label, :value => value).html_safe
395 395 end
396 396 else
397 397 l(:text_journal_deleted, :label => label, :old => old_value).html_safe
398 398 end
399 399 end
400 400
401 401 # Find the name of an associated record stored in the field attribute
402 402 def find_name_by_reflection(field, id)
403 403 unless id.present?
404 404 return nil
405 405 end
406 406 association = Issue.reflect_on_association(field.to_sym)
407 407 if association
408 408 record = association.class_name.constantize.find_by_id(id)
409 409 if record
410 410 record.name.force_encoding('UTF-8') if record.name.respond_to?(:force_encoding)
411 411 return record.name
412 412 end
413 413 end
414 414 end
415 415
416 416 # Renders issue children recursively
417 417 def render_api_issue_children(issue, api)
418 418 return if issue.leaf?
419 419 api.array :children do
420 420 issue.children.each do |child|
421 421 api.issue(:id => child.id) do
422 422 api.tracker(:id => child.tracker_id, :name => child.tracker.name) unless child.tracker.nil?
423 423 api.subject child.subject
424 424 render_api_issue_children(child, api)
425 425 end
426 426 end
427 427 end
428 428 end
429 429 end
@@ -1,747 +1,757
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 MailerTest < ActiveSupport::TestCase
21 21 include Redmine::I18n
22 22 include ActionDispatch::Assertions::SelectorAssertions
23 23 fixtures :projects, :enabled_modules, :issues, :users, :members,
24 24 :member_roles, :roles, :documents, :attachments, :news,
25 25 :tokens, :journals, :journal_details, :changesets,
26 26 :trackers, :projects_trackers,
27 27 :issue_statuses, :enumerations, :messages, :boards, :repositories,
28 28 :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions,
29 29 :versions,
30 30 :comments
31 31
32 32 def setup
33 33 ActionMailer::Base.deliveries.clear
34 34 Setting.host_name = 'mydomain.foo'
35 35 Setting.protocol = 'http'
36 36 Setting.plain_text_mail = '0'
37 37 end
38 38
39 39 def test_generated_links_in_emails
40 40 Setting.default_language = 'en'
41 41 Setting.host_name = 'mydomain.foo'
42 42 Setting.protocol = 'https'
43 43
44 44 journal = Journal.find(3)
45 45 assert Mailer.deliver_issue_edit(journal)
46 46
47 47 mail = last_email
48 48 assert_not_nil mail
49 49
50 50 assert_select_email do
51 51 # link to the main ticket
52 52 assert_select 'a[href=?]',
53 53 'https://mydomain.foo/issues/2#change-3',
54 54 :text => 'Feature request #2: Add ingredients categories'
55 55 # link to a referenced ticket
56 56 assert_select 'a[href=?][title=?]',
57 57 'https://mydomain.foo/issues/1',
58 58 'Can&#x27;t print recipes (New)',
59 59 :text => '#1'
60 60 # link to a changeset
61 61 assert_select 'a[href=?][title=?]',
62 62 'https://mydomain.foo/projects/ecookbook/repository/revisions/2',
63 63 'This commit fixes #1, #2 and references #1 &amp; #3',
64 64 :text => 'r2'
65 65 # link to a description diff
66 66 assert_select 'a[href=?][title=?]',
67 67 'https://mydomain.foo/journals/diff/3?detail_id=4',
68 68 'View differences',
69 69 :text => 'diff'
70 70 # link to an attachment
71 71 assert_select 'a[href=?]',
72 72 'https://mydomain.foo/attachments/download/4/source.rb',
73 73 :text => 'source.rb'
74 74 end
75 75 end
76 76
77 77 def test_generated_links_with_prefix
78 78 Setting.default_language = 'en'
79 79 relative_url_root = Redmine::Utils.relative_url_root
80 80 Setting.host_name = 'mydomain.foo/rdm'
81 81 Setting.protocol = 'http'
82 82
83 83 journal = Journal.find(3)
84 84 assert Mailer.deliver_issue_edit(journal)
85 85
86 86 mail = last_email
87 87 assert_not_nil mail
88 88
89 89 assert_select_email do
90 90 # link to the main ticket
91 91 assert_select 'a[href=?]',
92 92 'http://mydomain.foo/rdm/issues/2#change-3',
93 93 :text => 'Feature request #2: Add ingredients categories'
94 94 # link to a referenced ticket
95 95 assert_select 'a[href=?][title=?]',
96 96 'http://mydomain.foo/rdm/issues/1',
97 97 'Can&#x27;t print recipes (New)',
98 98 :text => '#1'
99 99 # link to a changeset
100 100 assert_select 'a[href=?][title=?]',
101 101 'http://mydomain.foo/rdm/projects/ecookbook/repository/revisions/2',
102 102 'This commit fixes #1, #2 and references #1 &amp; #3',
103 103 :text => 'r2'
104 104 # link to a description diff
105 105 assert_select 'a[href=?][title=?]',
106 106 'http://mydomain.foo/rdm/journals/diff/3?detail_id=4',
107 107 'View differences',
108 108 :text => 'diff'
109 109 # link to an attachment
110 110 assert_select 'a[href=?]',
111 111 'http://mydomain.foo/rdm/attachments/download/4/source.rb',
112 112 :text => 'source.rb'
113 113 end
114 114 end
115 115
116 def test_issue_edit_should_generate_url_with_hostname_for_relations
117 journal = Journal.new(:journalized => Issue.find(1), :user => User.find(1), :created_on => Time.now)
118 journal.details << JournalDetail.new(:property => 'relation', :prop_key => 'label_relates_to', :value => 2)
119 Mailer.deliver_issue_edit(journal)
120 assert_not_nil last_email
121 assert_select_email do
122 assert_select 'a[href=?]', 'http://mydomain.foo/issues/2', :text => 'Feature request #2'
123 end
124 end
125
116 126 def test_generated_links_with_prefix_and_no_relative_url_root
117 127 Setting.default_language = 'en'
118 128 relative_url_root = Redmine::Utils.relative_url_root
119 129 Setting.host_name = 'mydomain.foo/rdm'
120 130 Setting.protocol = 'http'
121 131 Redmine::Utils.relative_url_root = nil
122 132
123 133 journal = Journal.find(3)
124 134 assert Mailer.deliver_issue_edit(journal)
125 135
126 136 mail = last_email
127 137 assert_not_nil mail
128 138
129 139 assert_select_email do
130 140 # link to the main ticket
131 141 assert_select 'a[href=?]',
132 142 'http://mydomain.foo/rdm/issues/2#change-3',
133 143 :text => 'Feature request #2: Add ingredients categories'
134 144 # link to a referenced ticket
135 145 assert_select 'a[href=?][title=?]',
136 146 'http://mydomain.foo/rdm/issues/1',
137 147 'Can&#x27;t print recipes (New)',
138 148 :text => '#1'
139 149 # link to a changeset
140 150 assert_select 'a[href=?][title=?]',
141 151 'http://mydomain.foo/rdm/projects/ecookbook/repository/revisions/2',
142 152 'This commit fixes #1, #2 and references #1 &amp; #3',
143 153 :text => 'r2'
144 154 # link to a description diff
145 155 assert_select 'a[href=?][title=?]',
146 156 'http://mydomain.foo/rdm/journals/diff/3?detail_id=4',
147 157 'View differences',
148 158 :text => 'diff'
149 159 # link to an attachment
150 160 assert_select 'a[href=?]',
151 161 'http://mydomain.foo/rdm/attachments/download/4/source.rb',
152 162 :text => 'source.rb'
153 163 end
154 164 ensure
155 165 # restore it
156 166 Redmine::Utils.relative_url_root = relative_url_root
157 167 end
158 168
159 169 def test_email_headers
160 170 issue = Issue.find(1)
161 171 Mailer.deliver_issue_add(issue)
162 172 mail = last_email
163 173 assert_not_nil mail
164 174 assert_equal 'OOF', mail.header['X-Auto-Response-Suppress'].to_s
165 175 assert_equal 'auto-generated', mail.header['Auto-Submitted'].to_s
166 176 assert_equal '<redmine.example.net>', mail.header['List-Id'].to_s
167 177 end
168 178
169 179 def test_email_headers_should_include_sender
170 180 issue = Issue.find(1)
171 181 Mailer.deliver_issue_add(issue)
172 182 mail = last_email
173 183 assert_equal issue.author.login, mail.header['X-Redmine-Sender'].to_s
174 184 end
175 185
176 186 def test_plain_text_mail
177 187 Setting.plain_text_mail = 1
178 188 journal = Journal.find(2)
179 189 Mailer.deliver_issue_edit(journal)
180 190 mail = last_email
181 191 assert_equal "text/plain; charset=UTF-8", mail.content_type
182 192 assert_equal 0, mail.parts.size
183 193 assert !mail.encoded.include?('href')
184 194 end
185 195
186 196 def test_html_mail
187 197 Setting.plain_text_mail = 0
188 198 journal = Journal.find(2)
189 199 Mailer.deliver_issue_edit(journal)
190 200 mail = last_email
191 201 assert_equal 2, mail.parts.size
192 202 assert mail.encoded.include?('href')
193 203 end
194 204
195 205 def test_from_header
196 206 with_settings :mail_from => 'redmine@example.net' do
197 207 Mailer.test_email(User.find(1)).deliver
198 208 end
199 209 mail = last_email
200 210 assert_equal 'redmine@example.net', mail.from_addrs.first
201 211 end
202 212
203 213 def test_from_header_with_phrase
204 214 with_settings :mail_from => 'Redmine app <redmine@example.net>' do
205 215 Mailer.test_email(User.find(1)).deliver
206 216 end
207 217 mail = last_email
208 218 assert_equal 'redmine@example.net', mail.from_addrs.first
209 219 assert_equal 'Redmine app <redmine@example.net>', mail.header['From'].to_s
210 220 end
211 221
212 222 def test_should_not_send_email_without_recipient
213 223 news = News.first
214 224 user = news.author
215 225 # Remove members except news author
216 226 news.project.memberships.each {|m| m.destroy unless m.user == user}
217 227
218 228 user.pref.no_self_notified = false
219 229 user.pref.save
220 230 User.current = user
221 231 Mailer.news_added(news.reload).deliver
222 232 assert_equal 1, last_email.bcc.size
223 233
224 234 # nobody to notify
225 235 user.pref.no_self_notified = true
226 236 user.pref.save
227 237 User.current = user
228 238 ActionMailer::Base.deliveries.clear
229 239 Mailer.news_added(news.reload).deliver
230 240 assert ActionMailer::Base.deliveries.empty?
231 241 end
232 242
233 243 def test_issue_add_message_id
234 244 issue = Issue.find(2)
235 245 Mailer.deliver_issue_add(issue)
236 246 mail = last_email
237 247 assert_match /^redmine\.issue-2\.20060719190421\.[a-f0-9]+@example\.net/, mail.message_id
238 248 assert_include "redmine.issue-2.20060719190421@example.net", mail.references
239 249 end
240 250
241 251 def test_issue_edit_message_id
242 252 journal = Journal.find(3)
243 253 journal.issue = Issue.find(2)
244 254
245 255 Mailer.deliver_issue_edit(journal)
246 256 mail = last_email
247 257 assert_match /^redmine\.journal-3\.\d+\.[a-f0-9]+@example\.net/, mail.message_id
248 258 assert_include "redmine.issue-2.20060719190421@example.net", mail.references
249 259 assert_select_email do
250 260 # link to the update
251 261 assert_select "a[href=?]",
252 262 "http://mydomain.foo/issues/#{journal.journalized_id}#change-#{journal.id}"
253 263 end
254 264 end
255 265
256 266 def test_message_posted_message_id
257 267 message = Message.find(1)
258 268 Mailer.message_posted(message).deliver
259 269 mail = last_email
260 270 assert_match /^redmine\.message-1\.\d+\.[a-f0-9]+@example\.net/, mail.message_id
261 271 assert_include "redmine.message-1.20070512151532@example.net", mail.references
262 272 assert_select_email do
263 273 # link to the message
264 274 assert_select "a[href=?]",
265 275 "http://mydomain.foo/boards/#{message.board.id}/topics/#{message.id}",
266 276 :text => message.subject
267 277 end
268 278 end
269 279
270 280 def test_reply_posted_message_id
271 281 message = Message.find(3)
272 282 Mailer.message_posted(message).deliver
273 283 mail = last_email
274 284 assert_match /^redmine\.message-3\.\d+\.[a-f0-9]+@example\.net/, mail.message_id
275 285 assert_include "redmine.message-1.20070512151532@example.net", mail.references
276 286 assert_select_email do
277 287 # link to the reply
278 288 assert_select "a[href=?]",
279 289 "http://mydomain.foo/boards/#{message.board.id}/topics/#{message.root.id}?r=#{message.id}#message-#{message.id}",
280 290 :text => message.subject
281 291 end
282 292 end
283 293
284 294 test "#issue_add should notify project members" do
285 295 issue = Issue.find(1)
286 296 assert Mailer.deliver_issue_add(issue)
287 297 assert last_email.bcc.include?('dlopper@somenet.foo')
288 298 end
289 299
290 300 test "#issue_add should not notify project members that are not allow to view the issue" do
291 301 issue = Issue.find(1)
292 302 Role.find(2).remove_permission!(:view_issues)
293 303 assert Mailer.deliver_issue_add(issue)
294 304 assert !last_email.bcc.include?('dlopper@somenet.foo')
295 305 end
296 306
297 307 test "#issue_add should notify issue watchers" do
298 308 issue = Issue.find(1)
299 309 user = User.find(9)
300 310 # minimal email notification options
301 311 user.pref.no_self_notified = '1'
302 312 user.pref.save
303 313 user.mail_notification = false
304 314 user.save
305 315
306 316 Watcher.create!(:watchable => issue, :user => user)
307 317 assert Mailer.deliver_issue_add(issue)
308 318 assert last_email.bcc.include?(user.mail)
309 319 end
310 320
311 321 test "#issue_add should not notify watchers not allowed to view the issue" do
312 322 issue = Issue.find(1)
313 323 user = User.find(9)
314 324 Watcher.create!(:watchable => issue, :user => user)
315 325 Role.non_member.remove_permission!(:view_issues)
316 326 assert Mailer.deliver_issue_add(issue)
317 327 assert !last_email.bcc.include?(user.mail)
318 328 end
319 329
320 330 def test_issue_add_should_include_enabled_fields
321 331 Setting.default_language = 'en'
322 332 issue = Issue.find(2)
323 333 assert Mailer.deliver_issue_add(issue)
324 334 assert_mail_body_match '* Target version: 1.0', last_email
325 335 assert_select_email do
326 336 assert_select 'li', :text => 'Target version: 1.0'
327 337 end
328 338 end
329 339
330 340 def test_issue_add_should_not_include_disabled_fields
331 341 Setting.default_language = 'en'
332 342 issue = Issue.find(2)
333 343 tracker = issue.tracker
334 344 tracker.core_fields -= ['fixed_version_id']
335 345 tracker.save!
336 346 assert Mailer.deliver_issue_add(issue)
337 347 assert_mail_body_no_match 'Target version', last_email
338 348 assert_select_email do
339 349 assert_select 'li', :text => /Target version/, :count => 0
340 350 end
341 351 end
342 352
343 353 # test mailer methods for each language
344 354 def test_issue_add
345 355 issue = Issue.find(1)
346 356 valid_languages.each do |lang|
347 357 Setting.default_language = lang.to_s
348 358 assert Mailer.deliver_issue_add(issue)
349 359 end
350 360 end
351 361
352 362 def test_issue_edit
353 363 journal = Journal.find(1)
354 364 valid_languages.each do |lang|
355 365 Setting.default_language = lang.to_s
356 366 assert Mailer.deliver_issue_edit(journal)
357 367 end
358 368 end
359 369
360 370 def test_issue_edit_should_send_private_notes_to_users_with_permission_only
361 371 journal = Journal.find(1)
362 372 journal.private_notes = true
363 373 journal.save!
364 374
365 375 Role.find(2).add_permission! :view_private_notes
366 376 Mailer.deliver_issue_edit(journal)
367 377 assert_equal %w(dlopper@somenet.foo jsmith@somenet.foo), ActionMailer::Base.deliveries.last.bcc.sort
368 378
369 379 Role.find(2).remove_permission! :view_private_notes
370 380 Mailer.deliver_issue_edit(journal)
371 381 assert_equal %w(jsmith@somenet.foo), ActionMailer::Base.deliveries.last.bcc.sort
372 382 end
373 383
374 384 def test_issue_edit_should_send_private_notes_to_watchers_with_permission_only
375 385 Issue.find(1).set_watcher(User.find_by_login('someone'))
376 386 journal = Journal.find(1)
377 387 journal.private_notes = true
378 388 journal.save!
379 389
380 390 Role.non_member.add_permission! :view_private_notes
381 391 Mailer.deliver_issue_edit(journal)
382 392 assert_include 'someone@foo.bar', ActionMailer::Base.deliveries.last.bcc.sort
383 393
384 394 Role.non_member.remove_permission! :view_private_notes
385 395 Mailer.deliver_issue_edit(journal)
386 396 assert_not_include 'someone@foo.bar', ActionMailer::Base.deliveries.last.bcc.sort
387 397 end
388 398
389 399 def test_issue_edit_should_mark_private_notes
390 400 journal = Journal.find(2)
391 401 journal.private_notes = true
392 402 journal.save!
393 403
394 404 with_settings :default_language => 'en' do
395 405 Mailer.deliver_issue_edit(journal)
396 406 end
397 407 assert_mail_body_match '(Private notes)', last_email
398 408 end
399 409
400 410 def test_issue_edit_with_relation_should_notify_users_who_can_see_the_related_issue
401 411 issue = Issue.generate!
402 412 private_issue = Issue.generate!(:is_private => true)
403 413 IssueRelation.create!(:issue_from => issue, :issue_to => private_issue, :relation_type => 'relates')
404 414 issue.reload
405 415 assert_equal 1, issue.journals.size
406 416 journal = issue.journals.first
407 417 ActionMailer::Base.deliveries.clear
408 418
409 419 Mailer.deliver_issue_edit(journal)
410 420 last_email.bcc.each do |email|
411 421 user = User.find_by_mail(email)
412 422 assert private_issue.visible?(user), "Issue was not visible to #{user}"
413 423 end
414 424 end
415 425
416 426 def test_document_added
417 427 document = Document.find(1)
418 428 valid_languages.each do |lang|
419 429 Setting.default_language = lang.to_s
420 430 assert Mailer.document_added(document).deliver
421 431 end
422 432 end
423 433
424 434 def test_attachments_added
425 435 attachements = [ Attachment.find_by_container_type('Document') ]
426 436 valid_languages.each do |lang|
427 437 Setting.default_language = lang.to_s
428 438 assert Mailer.attachments_added(attachements).deliver
429 439 end
430 440 end
431 441
432 442 def test_version_file_added
433 443 attachements = [ Attachment.find_by_container_type('Version') ]
434 444 assert Mailer.attachments_added(attachements).deliver
435 445 assert_not_nil last_email.bcc
436 446 assert last_email.bcc.any?
437 447 assert_select_email do
438 448 assert_select "a[href=?]", "http://mydomain.foo/projects/ecookbook/files"
439 449 end
440 450 end
441 451
442 452 def test_project_file_added
443 453 attachements = [ Attachment.find_by_container_type('Project') ]
444 454 assert Mailer.attachments_added(attachements).deliver
445 455 assert_not_nil last_email.bcc
446 456 assert last_email.bcc.any?
447 457 assert_select_email do
448 458 assert_select "a[href=?]", "http://mydomain.foo/projects/ecookbook/files"
449 459 end
450 460 end
451 461
452 462 def test_news_added
453 463 news = News.first
454 464 valid_languages.each do |lang|
455 465 Setting.default_language = lang.to_s
456 466 assert Mailer.news_added(news).deliver
457 467 end
458 468 end
459 469
460 470 def test_news_comment_added
461 471 comment = Comment.find(2)
462 472 valid_languages.each do |lang|
463 473 Setting.default_language = lang.to_s
464 474 assert Mailer.news_comment_added(comment).deliver
465 475 end
466 476 end
467 477
468 478 def test_message_posted
469 479 message = Message.first
470 480 recipients = ([message.root] + message.root.children).collect {|m| m.author.mail if m.author}
471 481 recipients = recipients.compact.uniq
472 482 valid_languages.each do |lang|
473 483 Setting.default_language = lang.to_s
474 484 assert Mailer.message_posted(message).deliver
475 485 end
476 486 end
477 487
478 488 def test_wiki_content_added
479 489 content = WikiContent.find(1)
480 490 valid_languages.each do |lang|
481 491 Setting.default_language = lang.to_s
482 492 assert_difference 'ActionMailer::Base.deliveries.size' do
483 493 assert Mailer.wiki_content_added(content).deliver
484 494 assert_select_email do
485 495 assert_select 'a[href=?]',
486 496 'http://mydomain.foo/projects/ecookbook/wiki/CookBook_documentation',
487 497 :text => 'CookBook documentation'
488 498 end
489 499 end
490 500 end
491 501 end
492 502
493 503 def test_wiki_content_updated
494 504 content = WikiContent.find(1)
495 505 valid_languages.each do |lang|
496 506 Setting.default_language = lang.to_s
497 507 assert_difference 'ActionMailer::Base.deliveries.size' do
498 508 assert Mailer.wiki_content_updated(content).deliver
499 509 assert_select_email do
500 510 assert_select 'a[href=?]',
501 511 'http://mydomain.foo/projects/ecookbook/wiki/CookBook_documentation',
502 512 :text => 'CookBook documentation'
503 513 end
504 514 end
505 515 end
506 516 end
507 517
508 518 def test_account_information
509 519 user = User.find(2)
510 520 valid_languages.each do |lang|
511 521 user.update_attribute :language, lang.to_s
512 522 user.reload
513 523 assert Mailer.account_information(user, 'pAsswORd').deliver
514 524 end
515 525 end
516 526
517 527 def test_lost_password
518 528 token = Token.find(2)
519 529 valid_languages.each do |lang|
520 530 token.user.update_attribute :language, lang.to_s
521 531 token.reload
522 532 assert Mailer.lost_password(token).deliver
523 533 end
524 534 end
525 535
526 536 def test_register
527 537 token = Token.find(1)
528 538 Setting.host_name = 'redmine.foo'
529 539 Setting.protocol = 'https'
530 540
531 541 valid_languages.each do |lang|
532 542 token.user.update_attribute :language, lang.to_s
533 543 token.reload
534 544 ActionMailer::Base.deliveries.clear
535 545 assert Mailer.register(token).deliver
536 546 mail = last_email
537 547 assert_select_email do
538 548 assert_select "a[href=?]",
539 549 "https://redmine.foo/account/activate?token=#{token.value}",
540 550 :text => "https://redmine.foo/account/activate?token=#{token.value}"
541 551 end
542 552 end
543 553 end
544 554
545 555 def test_test
546 556 user = User.find(1)
547 557 valid_languages.each do |lang|
548 558 user.update_attribute :language, lang.to_s
549 559 assert Mailer.test_email(user).deliver
550 560 end
551 561 end
552 562
553 563 def test_reminders
554 564 Mailer.reminders(:days => 42)
555 565 assert_equal 1, ActionMailer::Base.deliveries.size
556 566 mail = last_email
557 567 assert mail.bcc.include?('dlopper@somenet.foo')
558 568 assert_mail_body_match 'Bug #3: Error 281 when updating a recipe', mail
559 569 assert_equal '1 issue(s) due in the next 42 days', mail.subject
560 570 end
561 571
562 572 def test_reminders_should_not_include_closed_issues
563 573 with_settings :default_language => 'en' do
564 574 Issue.create!(:project_id => 1, :tracker_id => 1, :status_id => 5,
565 575 :subject => 'Closed issue', :assigned_to_id => 3,
566 576 :due_date => 5.days.from_now,
567 577 :author_id => 2)
568 578 ActionMailer::Base.deliveries.clear
569 579
570 580 Mailer.reminders(:days => 42)
571 581 assert_equal 1, ActionMailer::Base.deliveries.size
572 582 mail = last_email
573 583 assert mail.bcc.include?('dlopper@somenet.foo')
574 584 assert_mail_body_no_match 'Closed issue', mail
575 585 end
576 586 end
577 587
578 588 def test_reminders_for_users
579 589 Mailer.reminders(:days => 42, :users => ['5'])
580 590 assert_equal 0, ActionMailer::Base.deliveries.size # No mail for dlopper
581 591 Mailer.reminders(:days => 42, :users => ['3'])
582 592 assert_equal 1, ActionMailer::Base.deliveries.size # No mail for dlopper
583 593 mail = last_email
584 594 assert mail.bcc.include?('dlopper@somenet.foo')
585 595 assert_mail_body_match 'Bug #3: Error 281 when updating a recipe', mail
586 596 end
587 597
588 598 def test_reminder_should_include_issues_assigned_to_groups
589 599 with_settings :default_language => 'en' do
590 600 group = Group.generate!
591 601 group.users << User.find(2)
592 602 group.users << User.find(3)
593 603
594 604 Issue.create!(:project_id => 1, :tracker_id => 1, :status_id => 1,
595 605 :subject => 'Assigned to group', :assigned_to => group,
596 606 :due_date => 5.days.from_now,
597 607 :author_id => 2)
598 608 ActionMailer::Base.deliveries.clear
599 609
600 610 Mailer.reminders(:days => 7)
601 611 assert_equal 2, ActionMailer::Base.deliveries.size
602 612 assert_equal %w(dlopper@somenet.foo jsmith@somenet.foo), ActionMailer::Base.deliveries.map(&:bcc).flatten.sort
603 613 ActionMailer::Base.deliveries.each do |mail|
604 614 assert_mail_body_match 'Assigned to group', mail
605 615 end
606 616 end
607 617 end
608 618
609 619 def test_mailer_should_not_change_locale
610 620 Setting.default_language = 'en'
611 621 # Set current language to italian
612 622 set_language_if_valid 'it'
613 623 # Send an email to a french user
614 624 user = User.find(1)
615 625 user.language = 'fr'
616 626 Mailer.account_activated(user).deliver
617 627 mail = last_email
618 628 assert_mail_body_match 'Votre compte', mail
619 629
620 630 assert_equal :it, current_language
621 631 end
622 632
623 633 def test_with_deliveries_off
624 634 Mailer.with_deliveries false do
625 635 Mailer.test_email(User.find(1)).deliver
626 636 end
627 637 assert ActionMailer::Base.deliveries.empty?
628 638 # should restore perform_deliveries
629 639 assert ActionMailer::Base.perform_deliveries
630 640 end
631 641
632 642 def test_layout_should_include_the_emails_header
633 643 with_settings :emails_header => "*Header content*" do
634 644 with_settings :plain_text_mail => 0 do
635 645 assert Mailer.test_email(User.find(1)).deliver
636 646 assert_select_email do
637 647 assert_select ".header" do
638 648 assert_select "strong", :text => "Header content"
639 649 end
640 650 end
641 651 end
642 652 with_settings :plain_text_mail => 1 do
643 653 assert Mailer.test_email(User.find(1)).deliver
644 654 mail = last_email
645 655 assert_not_nil mail
646 656 assert_include "*Header content*", mail.body.decoded
647 657 end
648 658 end
649 659 end
650 660
651 661 def test_layout_should_not_include_empty_emails_header
652 662 with_settings :emails_header => "", :plain_text_mail => 0 do
653 663 assert Mailer.test_email(User.find(1)).deliver
654 664 assert_select_email do
655 665 assert_select ".header", false
656 666 end
657 667 end
658 668 end
659 669
660 670 def test_layout_should_include_the_emails_footer
661 671 with_settings :emails_footer => "*Footer content*" do
662 672 with_settings :plain_text_mail => 0 do
663 673 assert Mailer.test_email(User.find(1)).deliver
664 674 assert_select_email do
665 675 assert_select ".footer" do
666 676 assert_select "strong", :text => "Footer content"
667 677 end
668 678 end
669 679 end
670 680 with_settings :plain_text_mail => 1 do
671 681 assert Mailer.test_email(User.find(1)).deliver
672 682 mail = last_email
673 683 assert_not_nil mail
674 684 assert_include "\n-- \n", mail.body.decoded
675 685 assert_include "*Footer content*", mail.body.decoded
676 686 end
677 687 end
678 688 end
679 689
680 690 def test_layout_should_not_include_empty_emails_footer
681 691 with_settings :emails_footer => "" do
682 692 with_settings :plain_text_mail => 0 do
683 693 assert Mailer.test_email(User.find(1)).deliver
684 694 assert_select_email do
685 695 assert_select ".footer", false
686 696 end
687 697 end
688 698 with_settings :plain_text_mail => 1 do
689 699 assert Mailer.test_email(User.find(1)).deliver
690 700 mail = last_email
691 701 assert_not_nil mail
692 702 assert_not_include "\n-- \n", mail.body.decoded
693 703 end
694 704 end
695 705 end
696 706
697 707 def test_should_escape_html_templates_only
698 708 Issue.generate!(:project_id => 1, :tracker_id => 1, :subject => 'Subject with a <tag>')
699 709 mail = last_email
700 710 assert_equal 2, mail.parts.size
701 711 assert_include '<tag>', text_part.body.encoded
702 712 assert_include '&lt;tag&gt;', html_part.body.encoded
703 713 end
704 714
705 715 def test_should_raise_delivery_errors_when_raise_delivery_errors_is_true
706 716 mail = Mailer.test_email(User.find(1))
707 717 mail.delivery_method.stubs(:deliver!).raises(Exception.new("delivery error"))
708 718
709 719 ActionMailer::Base.raise_delivery_errors = true
710 720 assert_raise Exception, "delivery error" do
711 721 mail.deliver
712 722 end
713 723 ensure
714 724 ActionMailer::Base.raise_delivery_errors = false
715 725 end
716 726
717 727 def test_should_log_delivery_errors_when_raise_delivery_errors_is_false
718 728 mail = Mailer.test_email(User.find(1))
719 729 mail.delivery_method.stubs(:deliver!).raises(Exception.new("delivery error"))
720 730
721 731 Rails.logger.expects(:error).with("Email delivery error: delivery error")
722 732 ActionMailer::Base.raise_delivery_errors = false
723 733 assert_nothing_raised do
724 734 mail.deliver
725 735 end
726 736 end
727 737
728 738 def test_mail_should_return_a_mail_message
729 739 assert_kind_of ::Mail::Message, Mailer.test_email(User.find(1))
730 740 end
731 741
732 742 private
733 743
734 744 def last_email
735 745 mail = ActionMailer::Base.deliveries.last
736 746 assert_not_nil mail
737 747 mail
738 748 end
739 749
740 750 def text_part
741 751 last_email.parts.detect {|part| part.content_type.include?('text/plain')}
742 752 end
743 753
744 754 def html_part
745 755 last_email.parts.detect {|part| part.content_type.include?('text/html')}
746 756 end
747 757 end
General Comments 0
You need to be logged in to leave comments. Login now