##// END OF EJS Templates
Mailer#url_for not called in views with Rails 3.1....
Jean-Philippe Lang -
r8903:e6b9ddad18fb
parent child
Show More
@@ -1,1127 +1,1127
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2011 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
28 28 extend Forwardable
29 29 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
30 30
31 31 # Return true if user is authorized for controller/action, otherwise false
32 32 def authorize_for(controller, action)
33 33 User.current.allowed_to?({:controller => controller, :action => action}, @project)
34 34 end
35 35
36 36 # Display a link if user is authorized
37 37 #
38 38 # @param [String] name Anchor text (passed to link_to)
39 39 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
40 40 # @param [optional, Hash] html_options Options passed to link_to
41 41 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
42 42 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
43 43 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
44 44 end
45 45
46 46 # Display a link to remote if user is authorized
47 47 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
48 48 url = options[:url] || {}
49 49 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
50 50 end
51 51
52 52 # Displays a link to user's account page if active
53 53 def link_to_user(user, options={})
54 54 if user.is_a?(User)
55 55 name = h(user.name(options[:format]))
56 56 if user.active?
57 57 link_to name, :controller => 'users', :action => 'show', :id => user
58 58 else
59 59 name
60 60 end
61 61 else
62 62 h(user.to_s)
63 63 end
64 64 end
65 65
66 66 # Displays a link to +issue+ with its subject.
67 67 # Examples:
68 68 #
69 69 # link_to_issue(issue) # => Defect #6: This is the subject
70 70 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
71 71 # link_to_issue(issue, :subject => false) # => Defect #6
72 72 # link_to_issue(issue, :project => true) # => Foo - Defect #6
73 73 #
74 74 def link_to_issue(issue, options={})
75 75 title = nil
76 76 subject = nil
77 77 if options[:subject] == false
78 78 title = truncate(issue.subject, :length => 60)
79 79 else
80 80 subject = issue.subject
81 81 if options[:truncate]
82 82 subject = truncate(subject, :length => options[:truncate])
83 83 end
84 84 end
85 85 s = link_to "#{h(issue.tracker)} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
86 86 :class => issue.css_classes,
87 87 :title => title
88 88 s << h(": #{subject}") if subject
89 89 s = h("#{issue.project} - ") + s if options[:project]
90 90 s
91 91 end
92 92
93 93 # Generates a link to an attachment.
94 94 # Options:
95 95 # * :text - Link text (default to attachment filename)
96 96 # * :download - Force download (default: false)
97 97 def link_to_attachment(attachment, options={})
98 98 text = options.delete(:text) || attachment.filename
99 99 action = options.delete(:download) ? 'download' : 'show'
100 100 link_to(h(text),
101 101 {:controller => 'attachments', :action => action,
102 :id => attachment, :filename => attachment.filename },
102 :id => attachment, :filename => attachment.filename }.merge(options),
103 103 options)
104 104 end
105 105
106 106 # Generates a link to a SCM revision
107 107 # Options:
108 108 # * :text - Link text (default to the formatted revision)
109 109 def link_to_revision(revision, repository, options={})
110 110 if repository.is_a?(Project)
111 111 repository = repository.repository
112 112 end
113 113 text = options.delete(:text) || format_revision(revision)
114 114 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
115 115 link_to(
116 116 h(text),
117 117 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
118 118 :title => l(:label_revision_id, format_revision(revision))
119 119 )
120 120 end
121 121
122 122 # Generates a link to a message
123 123 def link_to_message(message, options={}, html_options = nil)
124 124 link_to(
125 125 h(truncate(message.subject, :length => 60)),
126 126 { :controller => 'messages', :action => 'show',
127 127 :board_id => message.board_id,
128 128 :id => message.root,
129 129 :r => (message.parent_id && message.id),
130 130 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
131 131 }.merge(options),
132 132 html_options
133 133 )
134 134 end
135 135
136 136 # Generates a link to a project if active
137 137 # Examples:
138 138 #
139 139 # link_to_project(project) # => link to the specified project overview
140 140 # link_to_project(project, :action=>'settings') # => link to project settings
141 141 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
142 142 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
143 143 #
144 144 def link_to_project(project, options={}, html_options = nil)
145 145 if project.active?
146 146 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
147 147 link_to(h(project), url, html_options)
148 148 else
149 149 h(project)
150 150 end
151 151 end
152 152
153 153 def toggle_link(name, id, options={})
154 154 onclick = "Element.toggle('#{id}'); "
155 155 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
156 156 onclick << "return false;"
157 157 link_to(name, "#", :onclick => onclick)
158 158 end
159 159
160 160 def image_to_function(name, function, html_options = {})
161 161 html_options.symbolize_keys!
162 162 tag(:input, html_options.merge({
163 163 :type => "image", :src => image_path(name),
164 164 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
165 165 }))
166 166 end
167 167
168 168 def prompt_to_remote(name, text, param, url, html_options = {})
169 169 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
170 170 link_to name, {}, html_options
171 171 end
172 172
173 173 def format_activity_title(text)
174 174 h(truncate_single_line(text, :length => 100))
175 175 end
176 176
177 177 def format_activity_day(date)
178 178 date == Date.today ? l(:label_today).titleize : format_date(date)
179 179 end
180 180
181 181 def format_activity_description(text)
182 182 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
183 183 ).gsub(/[\r\n]+/, "<br />").html_safe
184 184 end
185 185
186 186 def format_version_name(version)
187 187 if version.project == @project
188 188 h(version)
189 189 else
190 190 h("#{version.project} - #{version}")
191 191 end
192 192 end
193 193
194 194 def due_date_distance_in_words(date)
195 195 if date
196 196 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
197 197 end
198 198 end
199 199
200 200 def render_page_hierarchy(pages, node=nil, options={})
201 201 content = ''
202 202 if pages[node]
203 203 content << "<ul class=\"pages-hierarchy\">\n"
204 204 pages[node].each do |page|
205 205 content << "<li>"
206 206 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
207 207 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
208 208 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
209 209 content << "</li>\n"
210 210 end
211 211 content << "</ul>\n"
212 212 end
213 213 content.html_safe
214 214 end
215 215
216 216 # Renders flash messages
217 217 def render_flash_messages
218 218 s = ''
219 219 flash.each do |k,v|
220 220 s << (content_tag('div', v.html_safe, :class => "flash #{k}"))
221 221 end
222 222 s.html_safe
223 223 end
224 224
225 225 # Renders tabs and their content
226 226 def render_tabs(tabs)
227 227 if tabs.any?
228 228 render :partial => 'common/tabs', :locals => {:tabs => tabs}
229 229 else
230 230 content_tag 'p', l(:label_no_data), :class => "nodata"
231 231 end
232 232 end
233 233
234 234 # Renders the project quick-jump box
235 235 def render_project_jump_box
236 236 return unless User.current.logged?
237 237 projects = User.current.memberships.collect(&:project).compact.uniq
238 238 if projects.any?
239 239 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
240 240 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
241 241 '<option value="" disabled="disabled">---</option>'
242 242 s << project_tree_options_for_select(projects, :selected => @project) do |p|
243 243 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
244 244 end
245 245 s << '</select>'
246 246 s.html_safe
247 247 end
248 248 end
249 249
250 250 def project_tree_options_for_select(projects, options = {})
251 251 s = ''
252 252 project_tree(projects) do |project, level|
253 253 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ').html_safe : '')
254 254 tag_options = {:value => project.id}
255 255 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
256 256 tag_options[:selected] = 'selected'
257 257 else
258 258 tag_options[:selected] = nil
259 259 end
260 260 tag_options.merge!(yield(project)) if block_given?
261 261 s << content_tag('option', name_prefix + h(project), tag_options)
262 262 end
263 263 s.html_safe
264 264 end
265 265
266 266 # Yields the given block for each project with its level in the tree
267 267 #
268 268 # Wrapper for Project#project_tree
269 269 def project_tree(projects, &block)
270 270 Project.project_tree(projects, &block)
271 271 end
272 272
273 273 def project_nested_ul(projects, &block)
274 274 s = ''
275 275 if projects.any?
276 276 ancestors = []
277 277 projects.sort_by(&:lft).each do |project|
278 278 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
279 279 s << "<ul>\n"
280 280 else
281 281 ancestors.pop
282 282 s << "</li>"
283 283 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
284 284 ancestors.pop
285 285 s << "</ul></li>\n"
286 286 end
287 287 end
288 288 s << "<li>"
289 289 s << yield(project).to_s
290 290 ancestors << project
291 291 end
292 292 s << ("</li></ul>\n" * ancestors.size)
293 293 end
294 294 s.html_safe
295 295 end
296 296
297 297 def principals_check_box_tags(name, principals)
298 298 s = ''
299 299 principals.sort.each do |principal|
300 300 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
301 301 end
302 302 s.html_safe
303 303 end
304 304
305 305 # Returns a string for users/groups option tags
306 306 def principals_options_for_select(collection, selected=nil)
307 307 s = ''
308 308 if collection.include?(User.current)
309 309 s << content_tag('option', "<< #{l(:label_me)} >>".html_safe, :value => User.current.id)
310 310 end
311 311 groups = ''
312 312 collection.sort.each do |element|
313 313 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected)
314 314 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
315 315 end
316 316 unless groups.empty?
317 317 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
318 318 end
319 319 s
320 320 end
321 321
322 322 # Truncates and returns the string as a single line
323 323 def truncate_single_line(string, *args)
324 324 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
325 325 end
326 326
327 327 # Truncates at line break after 250 characters or options[:length]
328 328 def truncate_lines(string, options={})
329 329 length = options[:length] || 250
330 330 if string.to_s =~ /\A(.{#{length}}.*?)$/m
331 331 "#{$1}..."
332 332 else
333 333 string
334 334 end
335 335 end
336 336
337 337 def anchor(text)
338 338 text.to_s.gsub(' ', '_')
339 339 end
340 340
341 341 def html_hours(text)
342 342 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
343 343 end
344 344
345 345 def authoring(created, author, options={})
346 346 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
347 347 end
348 348
349 349 def time_tag(time)
350 350 text = distance_of_time_in_words(Time.now, time)
351 351 if @project
352 352 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
353 353 else
354 354 content_tag('acronym', text, :title => format_time(time))
355 355 end
356 356 end
357 357
358 358 def syntax_highlight_lines(name, content)
359 359 lines = []
360 360 syntax_highlight(name, content).each_line { |line| lines << line }
361 361 lines
362 362 end
363 363
364 364 def syntax_highlight(name, content)
365 365 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
366 366 end
367 367
368 368 def to_path_param(path)
369 369 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
370 370 end
371 371
372 372 def pagination_links_full(paginator, count=nil, options={})
373 373 page_param = options.delete(:page_param) || :page
374 374 per_page_links = options.delete(:per_page_links)
375 375 url_param = params.dup
376 376
377 377 html = ''
378 378 if paginator.current.previous
379 379 # \xc2\xab(utf-8) = &#171;
380 380 html << link_to_content_update(
381 381 "\xc2\xab " + l(:label_previous),
382 382 url_param.merge(page_param => paginator.current.previous)) + ' '
383 383 end
384 384
385 385 html << (pagination_links_each(paginator, options) do |n|
386 386 link_to_content_update(n.to_s, url_param.merge(page_param => n))
387 387 end || '')
388 388
389 389 if paginator.current.next
390 390 # \xc2\xbb(utf-8) = &#187;
391 391 html << ' ' + link_to_content_update(
392 392 (l(:label_next) + " \xc2\xbb"),
393 393 url_param.merge(page_param => paginator.current.next))
394 394 end
395 395
396 396 unless count.nil?
397 397 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
398 398 if per_page_links != false && links = per_page_links(paginator.items_per_page)
399 399 html << " | #{links}"
400 400 end
401 401 end
402 402
403 403 html.html_safe
404 404 end
405 405
406 406 def per_page_links(selected=nil)
407 407 links = Setting.per_page_options_array.collect do |n|
408 408 n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
409 409 end
410 410 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
411 411 end
412 412
413 413 def reorder_links(name, url, method = :post)
414 414 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
415 415 url.merge({"#{name}[move_to]" => 'highest'}),
416 416 :method => method, :title => l(:label_sort_highest)) +
417 417 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
418 418 url.merge({"#{name}[move_to]" => 'higher'}),
419 419 :method => method, :title => l(:label_sort_higher)) +
420 420 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
421 421 url.merge({"#{name}[move_to]" => 'lower'}),
422 422 :method => method, :title => l(:label_sort_lower)) +
423 423 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
424 424 url.merge({"#{name}[move_to]" => 'lowest'}),
425 425 :method => method, :title => l(:label_sort_lowest))
426 426 end
427 427
428 428 def breadcrumb(*args)
429 429 elements = args.flatten
430 430 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
431 431 end
432 432
433 433 def other_formats_links(&block)
434 434 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
435 435 yield Redmine::Views::OtherFormatsBuilder.new(self)
436 436 concat('</p>'.html_safe)
437 437 end
438 438
439 439 def page_header_title
440 440 if @project.nil? || @project.new_record?
441 441 h(Setting.app_title)
442 442 else
443 443 b = []
444 444 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
445 445 if ancestors.any?
446 446 root = ancestors.shift
447 447 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
448 448 if ancestors.size > 2
449 449 b << "\xe2\x80\xa6"
450 450 ancestors = ancestors[-2, 2]
451 451 end
452 452 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
453 453 end
454 454 b << h(@project)
455 455 b.join(" \xc2\xbb ").html_safe
456 456 end
457 457 end
458 458
459 459 def html_title(*args)
460 460 if args.empty?
461 461 title = @html_title || []
462 462 title << @project.name if @project
463 463 title << Setting.app_title unless Setting.app_title == title.last
464 464 title.select {|t| !t.blank? }.join(' - ')
465 465 else
466 466 @html_title ||= []
467 467 @html_title += args
468 468 end
469 469 end
470 470
471 471 # Returns the theme, controller name, and action as css classes for the
472 472 # HTML body.
473 473 def body_css_classes
474 474 css = []
475 475 if theme = Redmine::Themes.theme(Setting.ui_theme)
476 476 css << 'theme-' + theme.name
477 477 end
478 478
479 479 css << 'controller-' + controller_name
480 480 css << 'action-' + action_name
481 481 css.join(' ')
482 482 end
483 483
484 484 def accesskey(s)
485 485 Redmine::AccessKeys.key_for s
486 486 end
487 487
488 488 # Formats text according to system settings.
489 489 # 2 ways to call this method:
490 490 # * with a String: textilizable(text, options)
491 491 # * with an object and one of its attribute: textilizable(issue, :description, options)
492 492 def textilizable(*args)
493 493 options = args.last.is_a?(Hash) ? args.pop : {}
494 494 case args.size
495 495 when 1
496 496 obj = options[:object]
497 497 text = args.shift
498 498 when 2
499 499 obj = args.shift
500 500 attr = args.shift
501 501 text = obj.send(attr).to_s
502 502 else
503 503 raise ArgumentError, 'invalid arguments to textilizable'
504 504 end
505 505 return '' if text.blank?
506 506 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
507 507 only_path = options.delete(:only_path) == false ? false : true
508 508
509 509 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
510 510
511 511 @parsed_headings = []
512 512 @heading_anchors = {}
513 513 @current_section = 0 if options[:edit_section_links]
514 514
515 515 parse_sections(text, project, obj, attr, only_path, options)
516 516 text = parse_non_pre_blocks(text) do |text|
517 517 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_macros].each do |method_name|
518 518 send method_name, text, project, obj, attr, only_path, options
519 519 end
520 520 end
521 521 parse_headings(text, project, obj, attr, only_path, options)
522 522
523 523 if @parsed_headings.any?
524 524 replace_toc(text, @parsed_headings)
525 525 end
526 526
527 527 text.html_safe
528 528 end
529 529
530 530 def parse_non_pre_blocks(text)
531 531 s = StringScanner.new(text)
532 532 tags = []
533 533 parsed = ''
534 534 while !s.eos?
535 535 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
536 536 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
537 537 if tags.empty?
538 538 yield text
539 539 end
540 540 parsed << text
541 541 if tag
542 542 if closing
543 543 if tags.last == tag.downcase
544 544 tags.pop
545 545 end
546 546 else
547 547 tags << tag.downcase
548 548 end
549 549 parsed << full_tag
550 550 end
551 551 end
552 552 # Close any non closing tags
553 553 while tag = tags.pop
554 554 parsed << "</#{tag}>"
555 555 end
556 556 parsed
557 557 end
558 558
559 559 def parse_inline_attachments(text, project, obj, attr, only_path, options)
560 560 # when using an image link, try to use an attachment, if possible
561 561 if options[:attachments] || (obj && obj.respond_to?(:attachments))
562 562 attachments = options[:attachments] || obj.attachments
563 563 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
564 564 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
565 565 # search for the picture in attachments
566 566 if found = Attachment.latest_attach(attachments, filename)
567 567 image_url = url_for :only_path => only_path, :controller => 'attachments',
568 568 :action => 'download', :id => found
569 569 desc = found.description.to_s.gsub('"', '')
570 570 if !desc.blank? && alttext.blank?
571 571 alt = " title=\"#{desc}\" alt=\"#{desc}\""
572 572 end
573 573 "src=\"#{image_url}\"#{alt}"
574 574 else
575 575 m
576 576 end
577 577 end
578 578 end
579 579 end
580 580
581 581 # Wiki links
582 582 #
583 583 # Examples:
584 584 # [[mypage]]
585 585 # [[mypage|mytext]]
586 586 # wiki links can refer other project wikis, using project name or identifier:
587 587 # [[project:]] -> wiki starting page
588 588 # [[project:|mytext]]
589 589 # [[project:mypage]]
590 590 # [[project:mypage|mytext]]
591 591 def parse_wiki_links(text, project, obj, attr, only_path, options)
592 592 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
593 593 link_project = project
594 594 esc, all, page, title = $1, $2, $3, $5
595 595 if esc.nil?
596 596 if page =~ /^([^\:]+)\:(.*)$/
597 597 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
598 598 page = $2
599 599 title ||= $1 if page.blank?
600 600 end
601 601
602 602 if link_project && link_project.wiki
603 603 # extract anchor
604 604 anchor = nil
605 605 if page =~ /^(.+?)\#(.+)$/
606 606 page, anchor = $1, $2
607 607 end
608 608 anchor = sanitize_anchor_name(anchor) if anchor.present?
609 609 # check if page exists
610 610 wiki_page = link_project.wiki.find_page(page)
611 611 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
612 612 "##{anchor}"
613 613 else
614 614 case options[:wiki_links]
615 615 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
616 616 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
617 617 else
618 618 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
619 619 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
620 620 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
621 621 :id => wiki_page_id, :anchor => anchor, :parent => parent)
622 622 end
623 623 end
624 624 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
625 625 else
626 626 # project or wiki doesn't exist
627 627 all
628 628 end
629 629 else
630 630 all
631 631 end
632 632 end
633 633 end
634 634
635 635 # Redmine links
636 636 #
637 637 # Examples:
638 638 # Issues:
639 639 # #52 -> Link to issue #52
640 640 # Changesets:
641 641 # r52 -> Link to revision 52
642 642 # commit:a85130f -> Link to scmid starting with a85130f
643 643 # Documents:
644 644 # document#17 -> Link to document with id 17
645 645 # document:Greetings -> Link to the document with title "Greetings"
646 646 # document:"Some document" -> Link to the document with title "Some document"
647 647 # Versions:
648 648 # version#3 -> Link to version with id 3
649 649 # version:1.0.0 -> Link to version named "1.0.0"
650 650 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
651 651 # Attachments:
652 652 # attachment:file.zip -> Link to the attachment of the current object named file.zip
653 653 # Source files:
654 654 # source:some/file -> Link to the file located at /some/file in the project's repository
655 655 # source:some/file@52 -> Link to the file's revision 52
656 656 # source:some/file#L120 -> Link to line 120 of the file
657 657 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
658 658 # export:some/file -> Force the download of the file
659 659 # Forum messages:
660 660 # message#1218 -> Link to message with id 1218
661 661 #
662 662 # Links can refer other objects from other projects, using project identifier:
663 663 # identifier:r52
664 664 # identifier:document:"Some document"
665 665 # identifier:version:1.0.0
666 666 # identifier:source:some/file
667 667 def parse_redmine_links(text, project, obj, attr, only_path, options)
668 668 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|
669 669 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
670 670 link = nil
671 671 if project_identifier
672 672 project = Project.visible.find_by_identifier(project_identifier)
673 673 end
674 674 if esc.nil?
675 675 if prefix.nil? && sep == 'r'
676 676 if project
677 677 repository = nil
678 678 if repo_identifier
679 679 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
680 680 else
681 681 repository = project.repository
682 682 end
683 683 # project.changesets.visible raises an SQL error because of a double join on repositories
684 684 if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier))
685 685 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},
686 686 :class => 'changeset',
687 687 :title => truncate_single_line(changeset.comments, :length => 100))
688 688 end
689 689 end
690 690 elsif sep == '#'
691 691 oid = identifier.to_i
692 692 case prefix
693 693 when nil
694 694 if issue = Issue.visible.find_by_id(oid, :include => :status)
695 695 anchor = comment_id ? "note-#{comment_id}" : nil
696 696 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor},
697 697 :class => issue.css_classes,
698 698 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
699 699 end
700 700 when 'document'
701 701 if document = Document.visible.find_by_id(oid)
702 702 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
703 703 :class => 'document'
704 704 end
705 705 when 'version'
706 706 if version = Version.visible.find_by_id(oid)
707 707 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
708 708 :class => 'version'
709 709 end
710 710 when 'message'
711 711 if message = Message.visible.find_by_id(oid, :include => :parent)
712 712 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
713 713 end
714 714 when 'forum'
715 715 if board = Board.visible.find_by_id(oid)
716 716 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
717 717 :class => 'board'
718 718 end
719 719 when 'news'
720 720 if news = News.visible.find_by_id(oid)
721 721 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
722 722 :class => 'news'
723 723 end
724 724 when 'project'
725 725 if p = Project.visible.find_by_id(oid)
726 726 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
727 727 end
728 728 end
729 729 elsif sep == ':'
730 730 # removes the double quotes if any
731 731 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
732 732 case prefix
733 733 when 'document'
734 734 if project && document = project.documents.visible.find_by_title(name)
735 735 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
736 736 :class => 'document'
737 737 end
738 738 when 'version'
739 739 if project && version = project.versions.visible.find_by_name(name)
740 740 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
741 741 :class => 'version'
742 742 end
743 743 when 'forum'
744 744 if project && board = project.boards.visible.find_by_name(name)
745 745 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
746 746 :class => 'board'
747 747 end
748 748 when 'news'
749 749 if project && news = project.news.visible.find_by_title(name)
750 750 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
751 751 :class => 'news'
752 752 end
753 753 when 'commit', 'source', 'export'
754 754 if project
755 755 repository = nil
756 756 if name =~ %r{^(([a-z0-9\-]+)\|)(.+)$}
757 757 repo_prefix, repo_identifier, name = $1, $2, $3
758 758 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
759 759 else
760 760 repository = project.repository
761 761 end
762 762 if prefix == 'commit'
763 763 if repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%"]))
764 764 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},
765 765 :class => 'changeset',
766 766 :title => truncate_single_line(h(changeset.comments), :length => 100)
767 767 end
768 768 else
769 769 if repository && User.current.allowed_to?(:browse_repository, project)
770 770 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
771 771 path, rev, anchor = $1, $3, $5
772 772 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => 'entry', :id => project, :repository_id => repository.identifier_param,
773 773 :path => to_path_param(path),
774 774 :rev => rev,
775 775 :anchor => anchor,
776 776 :format => (prefix == 'export' ? 'raw' : nil)},
777 777 :class => (prefix == 'export' ? 'source download' : 'source')
778 778 end
779 779 end
780 780 repo_prefix = nil
781 781 end
782 782 when 'attachment'
783 783 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
784 784 if attachments && attachment = attachments.detect {|a| a.filename == name }
785 785 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
786 786 :class => 'attachment'
787 787 end
788 788 when 'project'
789 789 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
790 790 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
791 791 end
792 792 end
793 793 end
794 794 end
795 795 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
796 796 end
797 797 end
798 798
799 799 HEADING_RE = /(<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>)/i unless const_defined?(:HEADING_RE)
800 800
801 801 def parse_sections(text, project, obj, attr, only_path, options)
802 802 return unless options[:edit_section_links]
803 803 text.gsub!(HEADING_RE) do
804 804 heading = $1
805 805 @current_section += 1
806 806 if @current_section > 1
807 807 content_tag('div',
808 808 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
809 809 :class => 'contextual',
810 810 :title => l(:button_edit_section)) + heading.html_safe
811 811 else
812 812 heading
813 813 end
814 814 end
815 815 end
816 816
817 817 # Headings and TOC
818 818 # Adds ids and links to headings unless options[:headings] is set to false
819 819 def parse_headings(text, project, obj, attr, only_path, options)
820 820 return if options[:headings] == false
821 821
822 822 text.gsub!(HEADING_RE) do
823 823 level, attrs, content = $2.to_i, $3, $4
824 824 item = strip_tags(content).strip
825 825 anchor = sanitize_anchor_name(item)
826 826 # used for single-file wiki export
827 827 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
828 828 @heading_anchors[anchor] ||= 0
829 829 idx = (@heading_anchors[anchor] += 1)
830 830 if idx > 1
831 831 anchor = "#{anchor}-#{idx}"
832 832 end
833 833 @parsed_headings << [level, anchor, item]
834 834 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
835 835 end
836 836 end
837 837
838 838 MACROS_RE = /
839 839 (!)? # escaping
840 840 (
841 841 \{\{ # opening tag
842 842 ([\w]+) # macro name
843 843 (\(([^\}]*)\))? # optional arguments
844 844 \}\} # closing tag
845 845 )
846 846 /x unless const_defined?(:MACROS_RE)
847 847
848 848 # Macros substitution
849 849 def parse_macros(text, project, obj, attr, only_path, options)
850 850 text.gsub!(MACROS_RE) do
851 851 esc, all, macro = $1, $2, $3.downcase
852 852 args = ($5 || '').split(',').each(&:strip)
853 853 if esc.nil?
854 854 begin
855 855 exec_macro(macro, obj, args)
856 856 rescue => e
857 857 "<div class=\"flash error\">Error executing the <strong>#{macro}</strong> macro (#{e})</div>"
858 858 end || all
859 859 else
860 860 all
861 861 end
862 862 end
863 863 end
864 864
865 865 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
866 866
867 867 # Renders the TOC with given headings
868 868 def replace_toc(text, headings)
869 869 text.gsub!(TOC_RE) do
870 870 if headings.empty?
871 871 ''
872 872 else
873 873 div_class = 'toc'
874 874 div_class << ' right' if $1 == '>'
875 875 div_class << ' left' if $1 == '<'
876 876 out = "<ul class=\"#{div_class}\"><li>"
877 877 root = headings.map(&:first).min
878 878 current = root
879 879 started = false
880 880 headings.each do |level, anchor, item|
881 881 if level > current
882 882 out << '<ul><li>' * (level - current)
883 883 elsif level < current
884 884 out << "</li></ul>\n" * (current - level) + "</li><li>"
885 885 elsif started
886 886 out << '</li><li>'
887 887 end
888 888 out << "<a href=\"##{anchor}\">#{item}</a>"
889 889 current = level
890 890 started = true
891 891 end
892 892 out << '</li></ul>' * (current - root)
893 893 out << '</li></ul>'
894 894 end
895 895 end
896 896 end
897 897
898 898 # Same as Rails' simple_format helper without using paragraphs
899 899 def simple_format_without_paragraph(text)
900 900 text.to_s.
901 901 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
902 902 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
903 903 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
904 904 html_safe
905 905 end
906 906
907 907 def lang_options_for_select(blank=true)
908 908 (blank ? [["(auto)", ""]] : []) +
909 909 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
910 910 end
911 911
912 912 def label_tag_for(name, option_tags = nil, options = {})
913 913 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
914 914 content_tag("label", label_text)
915 915 end
916 916
917 917 def labelled_tabular_form_for(*args, &proc)
918 918 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_tabular_form_for is deprecated and will be removed in Redmine 1.5. Use #labelled_form_for instead."
919 919 args << {} unless args.last.is_a?(Hash)
920 920 options = args.last
921 921 options[:html] ||= {}
922 922 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
923 923 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
924 924 form_for(*args, &proc)
925 925 end
926 926
927 927 def labelled_form_for(*args, &proc)
928 928 args << {} unless args.last.is_a?(Hash)
929 929 options = args.last
930 930 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
931 931 form_for(*args, &proc)
932 932 end
933 933
934 934 def labelled_fields_for(*args, &proc)
935 935 args << {} unless args.last.is_a?(Hash)
936 936 options = args.last
937 937 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
938 938 fields_for(*args, &proc)
939 939 end
940 940
941 941 def labelled_remote_form_for(*args, &proc)
942 942 args << {} unless args.last.is_a?(Hash)
943 943 options = args.last
944 944 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
945 945 remote_form_for(*args, &proc)
946 946 end
947 947
948 948 def error_messages_for(*objects)
949 949 html = ""
950 950 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
951 951 errors = objects.map {|o| o.errors.full_messages}.flatten
952 952 if errors.any?
953 953 html << "<div id='errorExplanation'><ul>\n"
954 954 errors.each do |error|
955 955 html << "<li>#{h error}</li>\n"
956 956 end
957 957 html << "</ul></div>\n"
958 958 end
959 959 html.html_safe
960 960 end
961 961
962 962 def back_url_hidden_field_tag
963 963 back_url = params[:back_url] || request.env['HTTP_REFERER']
964 964 back_url = CGI.unescape(back_url.to_s)
965 965 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
966 966 end
967 967
968 968 def check_all_links(form_name)
969 969 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
970 970 " | ".html_safe +
971 971 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
972 972 end
973 973
974 974 def progress_bar(pcts, options={})
975 975 pcts = [pcts, pcts] unless pcts.is_a?(Array)
976 976 pcts = pcts.collect(&:round)
977 977 pcts[1] = pcts[1] - pcts[0]
978 978 pcts << (100 - pcts[1] - pcts[0])
979 979 width = options[:width] || '100px;'
980 980 legend = options[:legend] || ''
981 981 content_tag('table',
982 982 content_tag('tr',
983 983 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
984 984 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
985 985 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
986 986 ), :class => 'progress', :style => "width: #{width};").html_safe +
987 987 content_tag('p', legend, :class => 'pourcent').html_safe
988 988 end
989 989
990 990 def checked_image(checked=true)
991 991 if checked
992 992 image_tag 'toggle_check.png'
993 993 end
994 994 end
995 995
996 996 def context_menu(url)
997 997 unless @context_menu_included
998 998 content_for :header_tags do
999 999 javascript_include_tag('context_menu') +
1000 1000 stylesheet_link_tag('context_menu')
1001 1001 end
1002 1002 if l(:direction) == 'rtl'
1003 1003 content_for :header_tags do
1004 1004 stylesheet_link_tag('context_menu_rtl')
1005 1005 end
1006 1006 end
1007 1007 @context_menu_included = true
1008 1008 end
1009 1009 javascript_tag "new ContextMenu('#{ url_for(url) }')"
1010 1010 end
1011 1011
1012 1012 def calendar_for(field_id)
1013 1013 include_calendar_headers_tags
1014 1014 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
1015 1015 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
1016 1016 end
1017 1017
1018 1018 def include_calendar_headers_tags
1019 1019 unless @calendar_headers_tags_included
1020 1020 @calendar_headers_tags_included = true
1021 1021 content_for :header_tags do
1022 1022 start_of_week = case Setting.start_of_week.to_i
1023 1023 when 1
1024 1024 'Calendar._FD = 1;' # Monday
1025 1025 when 7
1026 1026 'Calendar._FD = 0;' # Sunday
1027 1027 when 6
1028 1028 'Calendar._FD = 6;' # Saturday
1029 1029 else
1030 1030 '' # use language
1031 1031 end
1032 1032
1033 1033 javascript_include_tag('calendar/calendar') +
1034 1034 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
1035 1035 javascript_tag(start_of_week) +
1036 1036 javascript_include_tag('calendar/calendar-setup') +
1037 1037 stylesheet_link_tag('calendar')
1038 1038 end
1039 1039 end
1040 1040 end
1041 1041
1042 1042 def content_for(name, content = nil, &block)
1043 1043 @has_content ||= {}
1044 1044 @has_content[name] = true
1045 1045 super(name, content, &block)
1046 1046 end
1047 1047
1048 1048 def has_content?(name)
1049 1049 (@has_content && @has_content[name]) || false
1050 1050 end
1051 1051
1052 1052 def email_delivery_enabled?
1053 1053 !!ActionMailer::Base.perform_deliveries
1054 1054 end
1055 1055
1056 1056 # Returns the avatar image tag for the given +user+ if avatars are enabled
1057 1057 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1058 1058 def avatar(user, options = { })
1059 1059 if Setting.gravatar_enabled?
1060 1060 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
1061 1061 email = nil
1062 1062 if user.respond_to?(:mail)
1063 1063 email = user.mail
1064 1064 elsif user.to_s =~ %r{<(.+?)>}
1065 1065 email = $1
1066 1066 end
1067 1067 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1068 1068 else
1069 1069 ''
1070 1070 end
1071 1071 end
1072 1072
1073 1073 def sanitize_anchor_name(anchor)
1074 1074 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1075 1075 end
1076 1076
1077 1077 # Returns the javascript tags that are included in the html layout head
1078 1078 def javascript_heads
1079 1079 tags = javascript_include_tag(:defaults)
1080 1080 unless User.current.pref.warn_on_leaving_unsaved == '0'
1081 1081 tags << "\n".html_safe + javascript_tag("Event.observe(window, 'load', function(){ new WarnLeavingUnsaved('#{escape_javascript( l(:text_warn_on_leaving_unsaved) )}'); });")
1082 1082 end
1083 1083 tags
1084 1084 end
1085 1085
1086 1086 def favicon
1087 1087 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1088 1088 end
1089 1089
1090 1090 def robot_exclusion_tag
1091 1091 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1092 1092 end
1093 1093
1094 1094 # Returns true if arg is expected in the API response
1095 1095 def include_in_api_response?(arg)
1096 1096 unless @included_in_api_response
1097 1097 param = params[:include]
1098 1098 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1099 1099 @included_in_api_response.collect!(&:strip)
1100 1100 end
1101 1101 @included_in_api_response.include?(arg.to_s)
1102 1102 end
1103 1103
1104 1104 # Returns options or nil if nometa param or X-Redmine-Nometa header
1105 1105 # was set in the request
1106 1106 def api_meta(options)
1107 1107 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1108 1108 # compatibility mode for activeresource clients that raise
1109 1109 # an error when unserializing an array with attributes
1110 1110 nil
1111 1111 else
1112 1112 options
1113 1113 end
1114 1114 end
1115 1115
1116 1116 private
1117 1117
1118 1118 def wiki_helper
1119 1119 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1120 1120 extend helper
1121 1121 return self
1122 1122 end
1123 1123
1124 1124 def link_to_content_update(text, url_params = {}, html_options = {})
1125 1125 link_to(text, url_params, html_options)
1126 1126 end
1127 1127 end
@@ -1,345 +1,346
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2011 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))
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 s << content_tag('tr',
84 84 content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') +
85 85 content_tag('td', link_to_issue(child, :truncate => 60), :class => 'subject') +
86 86 content_tag('td', h(child.status)) +
87 87 content_tag('td', link_to_user(child.assigned_to)) +
88 88 content_tag('td', progress_bar(child.done_ratio, :width => '80px')),
89 89 :class => "issue issue-#{child.id} hascontextmenu #{level > 0 ? "idnt idnt-#{level}" : nil}")
90 90 end
91 91 s << '</table></form>'
92 92 s.html_safe
93 93 end
94 94
95 95 def render_custom_fields_rows(issue)
96 96 return if issue.custom_field_values.empty?
97 97 ordered_values = []
98 98 half = (issue.custom_field_values.size / 2.0).ceil
99 99 half.times do |i|
100 100 ordered_values << issue.custom_field_values[i]
101 101 ordered_values << issue.custom_field_values[i + half]
102 102 end
103 103 s = "<tr>\n"
104 104 n = 0
105 105 ordered_values.compact.each do |value|
106 106 s << "</tr>\n<tr>\n" if n > 0 && (n % 2) == 0
107 107 s << "\t<th>#{ h(value.custom_field.name) }:</th><td>#{ simple_format_without_paragraph(h(show_value(value))) }</td>\n"
108 108 n += 1
109 109 end
110 110 s << "</tr>\n"
111 111 s.html_safe
112 112 end
113 113
114 114 def issues_destroy_confirmation_message(issues)
115 115 issues = [issues] unless issues.is_a?(Array)
116 116 message = l(:text_issues_destroy_confirmation)
117 117 descendant_count = issues.inject(0) {|memo, i| memo += (i.right - i.left - 1)/2}
118 118 if descendant_count > 0
119 119 issues.each do |issue|
120 120 next if issue.root?
121 121 issues.each do |other_issue|
122 122 descendant_count -= 1 if issue.is_descendant_of?(other_issue)
123 123 end
124 124 end
125 125 if descendant_count > 0
126 126 message << "\n" + l(:text_issues_destroy_descendants_confirmation, :count => descendant_count)
127 127 end
128 128 end
129 129 message
130 130 end
131 131
132 132 def sidebar_queries
133 133 unless @sidebar_queries
134 134 @sidebar_queries = Query.visible.all(
135 135 :order => "#{Query.table_name}.name ASC",
136 136 # Project specific queries and global queries
137 137 :conditions => (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id])
138 138 )
139 139 end
140 140 @sidebar_queries
141 141 end
142 142
143 143 def query_links(title, queries)
144 144 # links to #index on issues/show
145 145 url_params = controller_name == 'issues' ? {:controller => 'issues', :action => 'index', :project_id => @project} : params
146 146
147 147 content_tag('h3', h(title)) +
148 148 queries.collect {|query|
149 149 css = 'query'
150 150 css << ' selected' if query == @query
151 151 link_to(h(query.name), url_params.merge(:query_id => query), :class => css)
152 152 }.join('<br />').html_safe
153 153 end
154 154
155 155 def render_sidebar_queries
156 156 out = ''.html_safe
157 157 queries = sidebar_queries.select {|q| !q.is_public?}
158 158 out << query_links(l(:label_my_queries), queries) if queries.any?
159 159 queries = sidebar_queries.select {|q| q.is_public?}
160 160 out << query_links(l(:label_query_plural), queries) if queries.any?
161 161 out
162 162 end
163 163
164 164 # Returns the textual representation of a journal details
165 165 # as an array of strings
166 def details_to_strings(details, no_html=false)
166 def details_to_strings(details, no_html=false, options={})
167 options[:only_path] = (options[:only_path] == false ? false : true)
167 168 strings = []
168 169 values_by_field = {}
169 170 details.each do |detail|
170 171 if detail.property == 'cf'
171 172 field_id = detail.prop_key
172 173 field = CustomField.find_by_id(field_id)
173 174 if field && field.multiple?
174 175 values_by_field[field_id] ||= {:added => [], :deleted => []}
175 176 if detail.old_value
176 177 values_by_field[field_id][:deleted] << detail.old_value
177 178 end
178 179 if detail.value
179 180 values_by_field[field_id][:added] << detail.value
180 181 end
181 182 next
182 183 end
183 184 end
184 strings << show_detail(detail, no_html)
185 strings << show_detail(detail, no_html, options)
185 186 end
186 187 values_by_field.each do |field_id, changes|
187 188 detail = JournalDetail.new(:property => 'cf', :prop_key => field_id)
188 189 if changes[:added].any?
189 190 detail.value = changes[:added]
190 strings << show_detail(detail, no_html)
191 strings << show_detail(detail, no_html, options)
191 192 elsif changes[:deleted].any?
192 193 detail.old_value = changes[:deleted]
193 strings << show_detail(detail, no_html)
194 strings << show_detail(detail, no_html, options)
194 195 end
195 196 end
196 197 strings
197 198 end
198 199
199 200 # Returns the textual representation of a single journal detail
200 def show_detail(detail, no_html=false)
201 def show_detail(detail, no_html=false, options={})
201 202 multiple = false
202 203 case detail.property
203 204 when 'attr'
204 205 field = detail.prop_key.to_s.gsub(/\_id$/, "")
205 206 label = l(("field_" + field).to_sym)
206 207 case detail.prop_key
207 208 when 'due_date', 'start_date'
208 209 value = format_date(detail.value.to_date) if detail.value
209 210 old_value = format_date(detail.old_value.to_date) if detail.old_value
210 211
211 212 when 'project_id', 'status_id', 'tracker_id', 'assigned_to_id',
212 213 'priority_id', 'category_id', 'fixed_version_id'
213 214 value = find_name_by_reflection(field, detail.value)
214 215 old_value = find_name_by_reflection(field, detail.old_value)
215 216
216 217 when 'estimated_hours'
217 218 value = "%0.02f" % detail.value.to_f unless detail.value.blank?
218 219 old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank?
219 220
220 221 when 'parent_id'
221 222 label = l(:field_parent_issue)
222 223 value = "##{detail.value}" unless detail.value.blank?
223 224 old_value = "##{detail.old_value}" unless detail.old_value.blank?
224 225
225 226 when 'is_private'
226 227 value = l(detail.value == "0" ? :general_text_No : :general_text_Yes) unless detail.value.blank?
227 228 old_value = l(detail.old_value == "0" ? :general_text_No : :general_text_Yes) unless detail.old_value.blank?
228 229 end
229 230 when 'cf'
230 231 custom_field = CustomField.find_by_id(detail.prop_key)
231 232 if custom_field
232 233 multiple = custom_field.multiple?
233 234 label = custom_field.name
234 235 value = format_value(detail.value, custom_field.field_format) if detail.value
235 236 old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value
236 237 end
237 238 when 'attachment'
238 239 label = l(:label_attachment)
239 240 end
240 241 call_hook(:helper_issues_show_detail_after_setting,
241 242 {:detail => detail, :label => label, :value => value, :old_value => old_value })
242 243
243 244 label ||= detail.prop_key
244 245 value ||= detail.value
245 246 old_value ||= detail.old_value
246 247
247 248 unless no_html
248 249 label = content_tag('strong', label)
249 250 old_value = content_tag("i", h(old_value)) if detail.old_value
250 251 old_value = content_tag("strike", old_value) if detail.old_value and detail.value.blank?
251 252 if detail.property == 'attachment' && !value.blank? && a = Attachment.find_by_id(detail.prop_key)
252 253 # Link to the attachment if it has not been removed
253 value = link_to_attachment(a, :download => true)
254 value = link_to_attachment(a, :download => true, :only_path => options[:only_path])
254 255 else
255 256 value = content_tag("i", h(value)) if value
256 257 end
257 258 end
258 259
259 260 if detail.property == 'attr' && detail.prop_key == 'description'
260 261 s = l(:text_journal_changed_no_detail, :label => label)
261 262 unless no_html
262 263 diff_link = link_to 'diff',
263 {:controller => 'journals', :action => 'diff', :id => detail.journal_id, :detail_id => detail.id},
264 {:controller => 'journals', :action => 'diff', :id => detail.journal_id, :detail_id => detail.id, :only_path => options[:only_path]},
264 265 :title => l(:label_view_diff)
265 266 s << " (#{ diff_link })"
266 267 end
267 268 s.html_safe
268 269 elsif detail.value.present?
269 270 case detail.property
270 271 when 'attr', 'cf'
271 272 if detail.old_value.present?
272 273 l(:text_journal_changed, :label => label, :old => old_value, :new => value).html_safe
273 274 elsif multiple
274 275 l(:text_journal_added, :label => label, :value => value).html_safe
275 276 else
276 277 l(:text_journal_set_to, :label => label, :value => value).html_safe
277 278 end
278 279 when 'attachment'
279 280 l(:text_journal_added, :label => label, :value => value).html_safe
280 281 end
281 282 else
282 283 l(:text_journal_deleted, :label => label, :old => old_value).html_safe
283 284 end
284 285 end
285 286
286 287 # Find the name of an associated record stored in the field attribute
287 288 def find_name_by_reflection(field, id)
288 289 association = Issue.reflect_on_association(field.to_sym)
289 290 if association
290 291 record = association.class_name.constantize.find_by_id(id)
291 292 return record.name if record
292 293 end
293 294 end
294 295
295 296 # Renders issue children recursively
296 297 def render_api_issue_children(issue, api)
297 298 return if issue.leaf?
298 299 api.array :children do
299 300 issue.children.each do |child|
300 301 api.issue(:id => child.id) do
301 302 api.tracker(:id => child.tracker_id, :name => child.tracker.name) unless child.tracker.nil?
302 303 api.subject child.subject
303 304 render_api_issue_children(child, api)
304 305 end
305 306 end
306 307 end
307 308 end
308 309
309 310 def issues_to_csv(issues, project, query, options={})
310 311 decimal_separator = l(:general_csv_decimal_separator)
311 312 encoding = l(:general_csv_encoding)
312 313 columns = (options[:columns] == 'all' ? query.available_columns : query.columns)
313 314
314 315 export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
315 316 # csv header fields
316 317 csv << [ "#" ] + columns.collect {|c| Redmine::CodesetUtil.from_utf8(c.caption.to_s, encoding) } +
317 318 (options[:description] ? [Redmine::CodesetUtil.from_utf8(l(:field_description), encoding)] : [])
318 319
319 320 # csv lines
320 321 issues.each do |issue|
321 322 col_values = columns.collect do |column|
322 323 s = if column.is_a?(QueryCustomFieldColumn)
323 324 cv = issue.custom_field_values.detect {|v| v.custom_field_id == column.custom_field.id}
324 325 show_value(cv)
325 326 else
326 327 value = issue.send(column.name)
327 328 if value.is_a?(Date)
328 329 format_date(value)
329 330 elsif value.is_a?(Time)
330 331 format_time(value)
331 332 elsif value.is_a?(Float)
332 333 ("%.2f" % value).gsub('.', decimal_separator)
333 334 else
334 335 value
335 336 end
336 337 end
337 338 s.to_s
338 339 end
339 340 csv << [ issue.id.to_s ] + col_values.collect {|c| Redmine::CodesetUtil.from_utf8(c.to_s, encoding) } +
340 341 (options[:description] ? [Redmine::CodesetUtil.from_utf8(issue.description, encoding)] : [])
341 342 end
342 343 end
343 344 export
344 345 end
345 346 end
@@ -1,482 +1,477
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Mailer < ActionMailer::Base
19 19 layout 'mailer'
20 20 helper :application
21 21 helper :issues
22 22 helper :custom_fields
23 23
24 24 include ActionController::UrlWriter
25 25 include Redmine::I18n
26 26
27 27 def self.default_url_options
28 28 h = Setting.host_name
29 29 h = h.to_s.gsub(%r{\/.*$}, '') unless Redmine::Utils.relative_url_root.blank?
30 30 { :host => h, :protocol => Setting.protocol }
31 31 end
32 32
33 def url_for(options)
34 options[:only_path] = false if options.is_a?(Hash)
35 super options
36 end
37
38 33 # Builds a tmail object used to email recipients of the added issue.
39 34 #
40 35 # Example:
41 36 # issue_add(issue) => tmail object
42 37 # Mailer.deliver_issue_add(issue) => sends an email to issue recipients
43 38 def issue_add(issue)
44 39 redmine_headers 'Project' => issue.project.identifier,
45 40 'Issue-Id' => issue.id,
46 41 'Issue-Author' => issue.author.login
47 42 redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
48 43 message_id issue
49 44 @author = issue.author
50 45 recipients issue.recipients
51 46 cc(issue.watcher_recipients - @recipients)
52 47 subject "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}"
53 48 body :issue => issue,
54 49 :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
55 50 render_multipart('issue_add', body)
56 51 end
57 52
58 53 # Builds a tmail object used to email recipients of the edited issue.
59 54 #
60 55 # Example:
61 56 # issue_edit(journal) => tmail object
62 57 # Mailer.deliver_issue_edit(journal) => sends an email to issue recipients
63 58 def issue_edit(journal)
64 59 issue = journal.journalized.reload
65 60 redmine_headers 'Project' => issue.project.identifier,
66 61 'Issue-Id' => issue.id,
67 62 'Issue-Author' => issue.author.login
68 63 redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
69 64 message_id journal
70 65 references issue
71 66 @author = journal.user
72 67 recipients issue.recipients
73 68 # Watchers in cc
74 69 cc(issue.watcher_recipients - @recipients)
75 70 s = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] "
76 71 s << "(#{issue.status.name}) " if journal.new_value_for('status_id')
77 72 s << issue.subject
78 73 subject s
79 74 body :issue => issue,
80 75 :journal => journal,
81 76 :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue, :anchor => "change-#{journal.id}")
82 77
83 78 render_multipart('issue_edit', body)
84 79 end
85 80
86 81 def reminder(user, issues, days)
87 82 set_language_if_valid user.language
88 83 recipients user.mail
89 84 subject l(:mail_subject_reminder, :count => issues.size, :days => days)
90 85 body :issues => issues,
91 86 :days => days,
92 87 :issues_url => url_for(:controller => 'issues', :action => 'index',
93 88 :set_filter => 1, :assigned_to_id => user.id,
94 89 :sort => 'due_date:asc')
95 90 render_multipart('reminder', body)
96 91 end
97 92
98 93 # Builds a tmail object used to email users belonging to the added document's project.
99 94 #
100 95 # Example:
101 96 # document_added(document) => tmail object
102 97 # Mailer.deliver_document_added(document) => sends an email to the document's project recipients
103 98 def document_added(document)
104 99 redmine_headers 'Project' => document.project.identifier
105 100 recipients document.recipients
106 101 subject "[#{document.project.name}] #{l(:label_document_new)}: #{document.title}"
107 102 body :document => document,
108 103 :document_url => url_for(:controller => 'documents', :action => 'show', :id => document)
109 104 render_multipart('document_added', body)
110 105 end
111 106
112 107 # Builds a tmail object used to email recipients of a project when an attachements are added.
113 108 #
114 109 # Example:
115 110 # attachments_added(attachments) => tmail object
116 111 # Mailer.deliver_attachments_added(attachments) => sends an email to the project's recipients
117 112 def attachments_added(attachments)
118 113 container = attachments.first.container
119 114 added_to = ''
120 115 added_to_url = ''
121 116 case container.class.name
122 117 when 'Project'
123 118 added_to_url = url_for(:controller => 'files', :action => 'index', :project_id => container)
124 119 added_to = "#{l(:label_project)}: #{container}"
125 120 recipients container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}.collect {|u| u.mail}
126 121 when 'Version'
127 122 added_to_url = url_for(:controller => 'files', :action => 'index', :project_id => container.project)
128 123 added_to = "#{l(:label_version)}: #{container.name}"
129 124 recipients container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}.collect {|u| u.mail}
130 125 when 'Document'
131 126 added_to_url = url_for(:controller => 'documents', :action => 'show', :id => container.id)
132 127 added_to = "#{l(:label_document)}: #{container.title}"
133 128 recipients container.recipients
134 129 end
135 130 redmine_headers 'Project' => container.project.identifier
136 131 subject "[#{container.project.name}] #{l(:label_attachment_new)}"
137 132 body :attachments => attachments,
138 133 :added_to => added_to,
139 134 :added_to_url => added_to_url
140 135 render_multipart('attachments_added', body)
141 136 end
142 137
143 138 # Builds a tmail object used to email recipients of a news' project when a news item is added.
144 139 #
145 140 # Example:
146 141 # news_added(news) => tmail object
147 142 # Mailer.deliver_news_added(news) => sends an email to the news' project recipients
148 143 def news_added(news)
149 144 redmine_headers 'Project' => news.project.identifier
150 145 message_id news
151 146 recipients news.recipients
152 147 subject "[#{news.project.name}] #{l(:label_news)}: #{news.title}"
153 148 body :news => news,
154 149 :news_url => url_for(:controller => 'news', :action => 'show', :id => news)
155 150 render_multipart('news_added', body)
156 151 end
157 152
158 153 # Builds a tmail object used to email recipients of a news' project when a news comment is added.
159 154 #
160 155 # Example:
161 156 # news_comment_added(comment) => tmail object
162 157 # Mailer.news_comment_added(comment) => sends an email to the news' project recipients
163 158 def news_comment_added(comment)
164 159 news = comment.commented
165 160 redmine_headers 'Project' => news.project.identifier
166 161 message_id comment
167 162 recipients news.recipients
168 163 cc news.watcher_recipients
169 164 subject "Re: [#{news.project.name}] #{l(:label_news)}: #{news.title}"
170 165 body :news => news,
171 166 :comment => comment,
172 167 :news_url => url_for(:controller => 'news', :action => 'show', :id => news)
173 168 render_multipart('news_comment_added', body)
174 169 end
175 170
176 171 # Builds a tmail object used to email the recipients of the specified message that was posted.
177 172 #
178 173 # Example:
179 174 # message_posted(message) => tmail object
180 175 # Mailer.deliver_message_posted(message) => sends an email to the recipients
181 176 def message_posted(message)
182 177 redmine_headers 'Project' => message.project.identifier,
183 178 'Topic-Id' => (message.parent_id || message.id)
184 179 message_id message
185 180 references message.parent unless message.parent.nil?
186 181 recipients(message.recipients)
187 182 cc((message.root.watcher_recipients + message.board.watcher_recipients).uniq - @recipients)
188 183 subject "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] #{message.subject}"
189 184 body :message => message,
190 185 :message_url => url_for(message.event_url)
191 186 render_multipart('message_posted', body)
192 187 end
193 188
194 189 # Builds a tmail object used to email the recipients of a project of the specified wiki content was added.
195 190 #
196 191 # Example:
197 192 # wiki_content_added(wiki_content) => tmail object
198 193 # Mailer.deliver_wiki_content_added(wiki_content) => sends an email to the project's recipients
199 194 def wiki_content_added(wiki_content)
200 195 redmine_headers 'Project' => wiki_content.project.identifier,
201 196 'Wiki-Page-Id' => wiki_content.page.id
202 197 message_id wiki_content
203 198 recipients wiki_content.recipients
204 199 cc(wiki_content.page.wiki.watcher_recipients - recipients)
205 200 subject "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_added, :id => wiki_content.page.pretty_title)}"
206 201 body :wiki_content => wiki_content,
207 202 :wiki_content_url => url_for(:controller => 'wiki', :action => 'show',
208 203 :project_id => wiki_content.project,
209 204 :id => wiki_content.page.title)
210 205 render_multipart('wiki_content_added', body)
211 206 end
212 207
213 208 # Builds a tmail object used to email the recipients of a project of the specified wiki content was updated.
214 209 #
215 210 # Example:
216 211 # wiki_content_updated(wiki_content) => tmail object
217 212 # Mailer.deliver_wiki_content_updated(wiki_content) => sends an email to the project's recipients
218 213 def wiki_content_updated(wiki_content)
219 214 redmine_headers 'Project' => wiki_content.project.identifier,
220 215 'Wiki-Page-Id' => wiki_content.page.id
221 216 message_id wiki_content
222 217 recipients wiki_content.recipients
223 218 cc(wiki_content.page.wiki.watcher_recipients + wiki_content.page.watcher_recipients - recipients)
224 219 subject "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_updated, :id => wiki_content.page.pretty_title)}"
225 220 body :wiki_content => wiki_content,
226 221 :wiki_content_url => url_for(:controller => 'wiki', :action => 'show',
227 222 :project_id => wiki_content.project,
228 223 :id => wiki_content.page.title),
229 224 :wiki_diff_url => url_for(:controller => 'wiki', :action => 'diff',
230 225 :project_id => wiki_content.project, :id => wiki_content.page.title,
231 226 :version => wiki_content.version)
232 227 render_multipart('wiki_content_updated', body)
233 228 end
234 229
235 230 # Builds a tmail object used to email the specified user their account information.
236 231 #
237 232 # Example:
238 233 # account_information(user, password) => tmail object
239 234 # Mailer.deliver_account_information(user, password) => sends account information to the user
240 235 def account_information(user, password)
241 236 set_language_if_valid user.language
242 237 recipients user.mail
243 238 subject l(:mail_subject_register, Setting.app_title)
244 239 body :user => user,
245 240 :password => password,
246 241 :login_url => url_for(:controller => 'account', :action => 'login')
247 242 render_multipart('account_information', body)
248 243 end
249 244
250 245 # Builds a tmail object used to email all active administrators of an account activation request.
251 246 #
252 247 # Example:
253 248 # account_activation_request(user) => tmail object
254 249 # Mailer.deliver_account_activation_request(user)=> sends an email to all active administrators
255 250 def account_activation_request(user)
256 251 # Send the email to all active administrators
257 252 recipients User.active.find(:all, :conditions => {:admin => true}).collect { |u| u.mail }.compact
258 253 subject l(:mail_subject_account_activation_request, Setting.app_title)
259 254 body :user => user,
260 255 :url => url_for(:controller => 'users', :action => 'index',
261 256 :status => User::STATUS_REGISTERED,
262 257 :sort_key => 'created_on', :sort_order => 'desc')
263 258 render_multipart('account_activation_request', body)
264 259 end
265 260
266 261 # Builds a tmail object used to email the specified user that their account was activated by an administrator.
267 262 #
268 263 # Example:
269 264 # account_activated(user) => tmail object
270 265 # Mailer.deliver_account_activated(user) => sends an email to the registered user
271 266 def account_activated(user)
272 267 set_language_if_valid user.language
273 268 recipients user.mail
274 269 subject l(:mail_subject_register, Setting.app_title)
275 270 body :user => user,
276 271 :login_url => url_for(:controller => 'account', :action => 'login')
277 272 render_multipart('account_activated', body)
278 273 end
279 274
280 275 def lost_password(token)
281 276 set_language_if_valid(token.user.language)
282 277 recipients token.user.mail
283 278 subject l(:mail_subject_lost_password, Setting.app_title)
284 279 body :token => token,
285 280 :url => url_for(:controller => 'account', :action => 'lost_password', :token => token.value)
286 281 render_multipart('lost_password', body)
287 282 end
288 283
289 284 def register(token)
290 285 set_language_if_valid(token.user.language)
291 286 recipients token.user.mail
292 287 subject l(:mail_subject_register, Setting.app_title)
293 288 body :token => token,
294 289 :url => url_for(:controller => 'account', :action => 'activate', :token => token.value)
295 290 render_multipart('register', body)
296 291 end
297 292
298 293 def test(user)
299 294 set_language_if_valid(user.language)
300 295 recipients user.mail
301 296 subject 'Redmine test'
302 297 body :url => url_for(:controller => 'welcome')
303 298 render_multipart('test', body)
304 299 end
305 300
306 301 # Overrides default deliver! method to prevent from sending an email
307 302 # with no recipient, cc or bcc
308 303 def deliver!(mail = @mail)
309 304 set_language_if_valid @initial_language
310 305 return false if (recipients.nil? || recipients.empty?) &&
311 306 (cc.nil? || cc.empty?) &&
312 307 (bcc.nil? || bcc.empty?)
313 308
314 309 # Set Message-Id and References
315 310 if @message_id_object
316 311 mail.message_id = self.class.message_id_for(@message_id_object)
317 312 end
318 313 if @references_objects
319 314 mail.references = @references_objects.collect {|o| self.class.message_id_for(o)}
320 315 end
321 316
322 317 # Log errors when raise_delivery_errors is set to false, Rails does not
323 318 raise_errors = self.class.raise_delivery_errors
324 319 self.class.raise_delivery_errors = true
325 320 begin
326 321 return super(mail)
327 322 rescue Exception => e
328 323 if raise_errors
329 324 raise e
330 325 elsif mylogger
331 326 mylogger.error "The following error occured while sending email notification: \"#{e.message}\". Check your configuration in config/configuration.yml."
332 327 end
333 328 ensure
334 329 self.class.raise_delivery_errors = raise_errors
335 330 end
336 331 end
337 332
338 333 # Sends reminders to issue assignees
339 334 # Available options:
340 335 # * :days => how many days in the future to remind about (defaults to 7)
341 336 # * :tracker => id of tracker for filtering issues (defaults to all trackers)
342 337 # * :project => id or identifier of project to process (defaults to all projects)
343 338 # * :users => array of user ids who should be reminded
344 339 def self.reminders(options={})
345 340 days = options[:days] || 7
346 341 project = options[:project] ? Project.find(options[:project]) : nil
347 342 tracker = options[:tracker] ? Tracker.find(options[:tracker]) : nil
348 343 user_ids = options[:users]
349 344
350 345 scope = Issue.open.scoped(:conditions => ["#{Issue.table_name}.assigned_to_id IS NOT NULL" +
351 346 " AND #{Project.table_name}.status = #{Project::STATUS_ACTIVE}" +
352 347 " AND #{Issue.table_name}.due_date <= ?", days.day.from_now.to_date]
353 348 )
354 349 scope = scope.scoped(:conditions => {:assigned_to_id => user_ids}) if user_ids.present?
355 350 scope = scope.scoped(:conditions => {:project_id => project.id}) if project
356 351 scope = scope.scoped(:conditions => {:tracker_id => tracker.id}) if tracker
357 352
358 353 issues_by_assignee = scope.all(:include => [:status, :assigned_to, :project, :tracker]).group_by(&:assigned_to)
359 354 issues_by_assignee.each do |assignee, issues|
360 355 deliver_reminder(assignee, issues, days) if assignee && assignee.active?
361 356 end
362 357 end
363 358
364 359 # Activates/desactivates email deliveries during +block+
365 360 def self.with_deliveries(enabled = true, &block)
366 361 was_enabled = ActionMailer::Base.perform_deliveries
367 362 ActionMailer::Base.perform_deliveries = !!enabled
368 363 yield
369 364 ensure
370 365 ActionMailer::Base.perform_deliveries = was_enabled
371 366 end
372 367
373 368 private
374 369 def initialize_defaults(method_name)
375 370 super
376 371 @initial_language = current_language
377 372 set_language_if_valid Setting.default_language
378 373 from Setting.mail_from
379 374
380 375 # Common headers
381 376 headers 'X-Mailer' => 'Redmine',
382 377 'X-Redmine-Host' => Setting.host_name,
383 378 'X-Redmine-Site' => Setting.app_title,
384 379 'X-Auto-Response-Suppress' => 'OOF',
385 380 'Auto-Submitted' => 'auto-generated'
386 381 end
387 382
388 383 # Appends a Redmine header field (name is prepended with 'X-Redmine-')
389 384 def redmine_headers(h)
390 385 h.each { |k,v| headers["X-Redmine-#{k}"] = v.to_s }
391 386 end
392 387
393 388 # Overrides the create_mail method
394 389 def create_mail
395 390 # Removes the current user from the recipients and cc
396 391 # if he doesn't want to receive notifications about what he does
397 392 @author ||= User.current
398 393 if @author.pref[:no_self_notified]
399 394 recipients.delete(@author.mail) if recipients
400 395 cc.delete(@author.mail) if cc
401 396 end
402 397
403 398 if @author.logged?
404 399 redmine_headers 'Sender' => @author.login
405 400 end
406 401
407 402 notified_users = [recipients, cc].flatten.compact.uniq
408 403 # Rails would log recipients only, not cc and bcc
409 404 mylogger.info "Sending email notification to: #{notified_users.join(', ')}" if mylogger
410 405
411 406 # Blind carbon copy recipients
412 407 if Setting.bcc_recipients?
413 408 bcc(notified_users)
414 409 recipients []
415 410 cc []
416 411 end
417 412 super
418 413 end
419 414
420 415 # Rails 2.3 has problems rendering implicit multipart messages with
421 416 # layouts so this method will wrap an multipart messages with
422 417 # explicit parts.
423 418 #
424 419 # https://rails.lighthouseapp.com/projects/8994/tickets/2338-actionmailer-mailer-views-and-content-type
425 420 # https://rails.lighthouseapp.com/projects/8994/tickets/1799-actionmailer-doesnt-set-template_format-when-rendering-layouts
426 421
427 422 def render_multipart(method_name, body)
428 423 if Setting.plain_text_mail?
429 424 content_type "text/plain"
430 425 body render(:file => "#{method_name}.text.erb",
431 426 :body => body,
432 427 :layout => 'mailer.text.erb')
433 428 else
434 429 content_type "multipart/alternative"
435 430 part :content_type => "text/plain",
436 431 :body => render(:file => "#{method_name}.text.erb",
437 432 :body => body, :layout => 'mailer.text.erb')
438 433 part :content_type => "text/html",
439 434 :body => render_message("#{method_name}.html.erb", body)
440 435 end
441 436 end
442 437
443 438 # Makes partial rendering work with Rails 1.2 (retro-compatibility)
444 439 def self.controller_path
445 440 ''
446 441 end unless respond_to?('controller_path')
447 442
448 443 # Returns a predictable Message-Id for the given object
449 444 def self.message_id_for(object)
450 445 # id + timestamp should reduce the odds of a collision
451 446 # as far as we don't send multiple emails for the same object
452 447 timestamp = object.send(object.respond_to?(:created_on) ? :created_on : :updated_on)
453 448 hash = "redmine.#{object.class.name.demodulize.underscore}-#{object.id}.#{timestamp.strftime("%Y%m%d%H%M%S")}"
454 449 host = Setting.mail_from.to_s.gsub(%r{^.*@}, '')
455 450 host = "#{::Socket.gethostname}.redmine" if host.empty?
456 451 "<#{hash}@#{host}>"
457 452 end
458 453
459 454 private
460 455
461 456 def message_id(object)
462 457 @message_id_object = object
463 458 end
464 459
465 460 def references(object)
466 461 @references_objects ||= []
467 462 @references_objects << object
468 463 end
469 464
470 465 def mylogger
471 466 Rails.logger
472 467 end
473 468 end
474 469
475 470 # Patch TMail so that message_id is not overwritten
476 471 module TMail
477 472 class Mail
478 473 def add_message_id( fqdn = nil )
479 474 self.message_id ||= ::TMail::new_message_id(fqdn)
480 475 end
481 476 end
482 477 end
@@ -1,15 +1,15
1 1 <h1><%= link_to(h("#{issue.tracker.name} ##{issue.id}: #{issue.subject}"), issue_url) %></h1>
2 2
3 3 <ul>
4 4 <li><%=l(:field_author)%>: <%=h issue.author %></li>
5 5 <li><%=l(:field_status)%>: <%=h issue.status %></li>
6 6 <li><%=l(:field_priority)%>: <%=h issue.priority %></li>
7 7 <li><%=l(:field_assigned_to)%>: <%=h issue.assigned_to %></li>
8 8 <li><%=l(:field_category)%>: <%=h issue.category %></li>
9 9 <li><%=l(:field_fixed_version)%>: <%=h issue.fixed_version %></li>
10 10 <% issue.custom_field_values.each do |c| %>
11 11 <li><%=h c.custom_field.name %>: <%=h show_value(c) %></li>
12 12 <% end %>
13 13 </ul>
14 14
15 <%= textilizable(issue, :description) %>
15 <%= textilizable(issue, :description, :only_path => false) %>
@@ -1,3 +1,3
1 1 <%= link_to(h(@document.title), @document_url) %> (<%=h @document.category.name %>)<br />
2 2 <br />
3 <%= textilizable(@document, :description) %>
3 <%= textilizable(@document, :description, :only_path => false) %>
@@ -1,11 +1,11
1 1 <%= l(:text_issue_updated, :id => "##{@issue.id}", :author => h(@journal.user)) %>
2 2
3 3 <ul>
4 <% details_to_strings(@journal.details).each do |string| %>
4 <% details_to_strings(@journal.details, false, :only_path => false).each do |string| %>
5 5 <li><%= string %></li>
6 6 <% end %>
7 7 </ul>
8 8
9 <%= textilizable(@journal, :notes) %>
9 <%= textilizable(@journal, :notes, :only_path => false) %>
10 10 <hr />
11 11 <%= render :partial => "issue.html.erb", :locals => { :issue => @issue, :issue_url => @issue_url } %>
@@ -1,4 +1,4
1 1 <h1><%=h @message.board.project.name %> - <%=h @message.board.name %>: <%= link_to(h(@message.subject), @message_url) %></h1>
2 2 <em><%=h @message.author %></em>
3 3
4 <%= textilizable(@message, :content) %>
4 <%= textilizable(@message, :content, :only_path => false) %>
@@ -1,4 +1,4
1 1 <h1><%= link_to(h(@news.title), @news_url) %></h1>
2 2 <em><%=h @news.author.name %></em>
3 3
4 <%= textilizable(@news, :description) %>
4 <%= textilizable(@news, :description, :only_path => false) %>
@@ -1,5 +1,5
1 1 <h1><%= link_to(h(@news.title), @news_url) %></h1>
2 2
3 3 <p><%= l(:text_user_wrote, :value => h(@comment.author)) %></p>
4 4
5 <%= textilizable @comment, :comments %>
5 <%= textilizable @comment, :comments, :only_path => false %>
@@ -1,9 +1,9
1 1 <p><%= l(:mail_body_reminder, :count => @issues.size, :days => @days) %></p>
2 2
3 3 <ul>
4 4 <% @issues.each do |issue| -%>
5 <li><%=h issue.project %> - <%=link_to(h("#{issue.tracker} ##{issue.id}"), :controller => 'issues', :action => 'show', :id => issue)%>: <%=h issue.subject %></li>
5 <li><%=h issue.project %> - <%=link_to(h("#{issue.tracker} ##{issue.id}"), :controller => 'issues', :action => 'show', :id => issue, :only_path => false)%>: <%=h issue.subject %></li>
6 6 <% end -%>
7 7 </ul>
8 8
9 9 <p><%= link_to l(:label_issue_view_all), @issues_url %></p>
General Comments 0
You need to be logged in to leave comments. Login now