##// END OF EJS Templates
Adds tracker name to Redmine issue link titles (#13946)....
Jean-Philippe Lang -
r14238:a7b6ad9bb5ba
parent child
Show More
@@ -1,1335 +1,1335
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2015 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 include Redmine::SudoMode::Helper
29 29
30 30 extend Forwardable
31 31 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
32 32
33 33 # Return true if user is authorized for controller/action, otherwise false
34 34 def authorize_for(controller, action)
35 35 User.current.allowed_to?({:controller => controller, :action => action}, @project)
36 36 end
37 37
38 38 # Display a link if user is authorized
39 39 #
40 40 # @param [String] name Anchor text (passed to link_to)
41 41 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
42 42 # @param [optional, Hash] html_options Options passed to link_to
43 43 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
44 44 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
45 45 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
46 46 end
47 47
48 48 # Displays a link to user's account page if active
49 49 def link_to_user(user, options={})
50 50 if user.is_a?(User)
51 51 name = h(user.name(options[:format]))
52 52 if user.active? || (User.current.admin? && user.logged?)
53 53 link_to name, user_path(user), :class => user.css_classes
54 54 else
55 55 name
56 56 end
57 57 else
58 58 h(user.to_s)
59 59 end
60 60 end
61 61
62 62 # Displays a link to +issue+ with its subject.
63 63 # Examples:
64 64 #
65 65 # link_to_issue(issue) # => Defect #6: This is the subject
66 66 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
67 67 # link_to_issue(issue, :subject => false) # => Defect #6
68 68 # link_to_issue(issue, :project => true) # => Foo - Defect #6
69 69 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
70 70 #
71 71 def link_to_issue(issue, options={})
72 72 title = nil
73 73 subject = nil
74 74 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
75 75 if options[:subject] == false
76 76 title = issue.subject.truncate(60)
77 77 else
78 78 subject = issue.subject
79 79 if truncate_length = options[:truncate]
80 80 subject = subject.truncate(truncate_length)
81 81 end
82 82 end
83 83 only_path = options[:only_path].nil? ? true : options[:only_path]
84 84 s = link_to(text, issue_url(issue, :only_path => only_path),
85 85 :class => issue.css_classes, :title => title)
86 86 s << h(": #{subject}") if subject
87 87 s = h("#{issue.project} - ") + s if options[:project]
88 88 s
89 89 end
90 90
91 91 # Generates a link to an attachment.
92 92 # Options:
93 93 # * :text - Link text (default to attachment filename)
94 94 # * :download - Force download (default: false)
95 95 def link_to_attachment(attachment, options={})
96 96 text = options.delete(:text) || attachment.filename
97 97 route_method = options.delete(:download) ? :download_named_attachment_url : :named_attachment_url
98 98 html_options = options.slice!(:only_path)
99 99 options[:only_path] = true unless options.key?(:only_path)
100 100 url = send(route_method, attachment, attachment.filename, options)
101 101 link_to text, url, html_options
102 102 end
103 103
104 104 # Generates a link to a SCM revision
105 105 # Options:
106 106 # * :text - Link text (default to the formatted revision)
107 107 def link_to_revision(revision, repository, options={})
108 108 if repository.is_a?(Project)
109 109 repository = repository.repository
110 110 end
111 111 text = options.delete(:text) || format_revision(revision)
112 112 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
113 113 link_to(
114 114 h(text),
115 115 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
116 116 :title => l(:label_revision_id, format_revision(revision)),
117 117 :accesskey => options[:accesskey]
118 118 )
119 119 end
120 120
121 121 # Generates a link to a message
122 122 def link_to_message(message, options={}, html_options = nil)
123 123 link_to(
124 124 message.subject.truncate(60),
125 125 board_message_url(message.board_id, message.parent_id || message.id, {
126 126 :r => (message.parent_id && message.id),
127 127 :anchor => (message.parent_id ? "message-#{message.id}" : nil),
128 128 :only_path => true
129 129 }.merge(options)),
130 130 html_options
131 131 )
132 132 end
133 133
134 134 # Generates a link to a project if active
135 135 # Examples:
136 136 #
137 137 # link_to_project(project) # => link to the specified project overview
138 138 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
139 139 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
140 140 #
141 141 def link_to_project(project, options={}, html_options = nil)
142 142 if project.archived?
143 143 h(project.name)
144 144 else
145 145 link_to project.name,
146 146 project_url(project, {:only_path => true}.merge(options)),
147 147 html_options
148 148 end
149 149 end
150 150
151 151 # Generates a link to a project settings if active
152 152 def link_to_project_settings(project, options={}, html_options=nil)
153 153 if project.active?
154 154 link_to project.name, settings_project_path(project, options), html_options
155 155 elsif project.archived?
156 156 h(project.name)
157 157 else
158 158 link_to project.name, project_path(project, options), html_options
159 159 end
160 160 end
161 161
162 162 # Generates a link to a version
163 163 def link_to_version(version, options = {})
164 164 return '' unless version && version.is_a?(Version)
165 165 options = {:title => format_date(version.effective_date)}.merge(options)
166 166 link_to_if version.visible?, format_version_name(version), version_path(version), options
167 167 end
168 168
169 169 # Helper that formats object for html or text rendering
170 170 def format_object(object, html=true, &block)
171 171 if block_given?
172 172 object = yield object
173 173 end
174 174 case object.class.name
175 175 when 'Array'
176 176 object.map {|o| format_object(o, html)}.join(', ').html_safe
177 177 when 'Time'
178 178 format_time(object)
179 179 when 'Date'
180 180 format_date(object)
181 181 when 'Fixnum'
182 182 object.to_s
183 183 when 'Float'
184 184 sprintf "%.2f", object
185 185 when 'User'
186 186 html ? link_to_user(object) : object.to_s
187 187 when 'Project'
188 188 html ? link_to_project(object) : object.to_s
189 189 when 'Version'
190 190 html ? link_to_version(object) : object.to_s
191 191 when 'TrueClass'
192 192 l(:general_text_Yes)
193 193 when 'FalseClass'
194 194 l(:general_text_No)
195 195 when 'Issue'
196 196 object.visible? && html ? link_to_issue(object) : "##{object.id}"
197 197 when 'CustomValue', 'CustomFieldValue'
198 198 if object.custom_field
199 199 f = object.custom_field.format.formatted_custom_value(self, object, html)
200 200 if f.nil? || f.is_a?(String)
201 201 f
202 202 else
203 203 format_object(f, html, &block)
204 204 end
205 205 else
206 206 object.value.to_s
207 207 end
208 208 else
209 209 html ? h(object) : object.to_s
210 210 end
211 211 end
212 212
213 213 def wiki_page_path(page, options={})
214 214 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
215 215 end
216 216
217 217 def thumbnail_tag(attachment)
218 218 link_to image_tag(thumbnail_path(attachment)),
219 219 named_attachment_path(attachment, attachment.filename),
220 220 :title => attachment.filename
221 221 end
222 222
223 223 def toggle_link(name, id, options={})
224 224 onclick = "$('##{id}').toggle(); "
225 225 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
226 226 onclick << "return false;"
227 227 link_to(name, "#", :onclick => onclick)
228 228 end
229 229
230 230 def format_activity_title(text)
231 231 h(truncate_single_line_raw(text, 100))
232 232 end
233 233
234 234 def format_activity_day(date)
235 235 date == User.current.today ? l(:label_today).titleize : format_date(date)
236 236 end
237 237
238 238 def format_activity_description(text)
239 239 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
240 240 ).gsub(/[\r\n]+/, "<br />").html_safe
241 241 end
242 242
243 243 def format_version_name(version)
244 244 if version.project == @project
245 245 h(version)
246 246 else
247 247 h("#{version.project} - #{version}")
248 248 end
249 249 end
250 250
251 251 def due_date_distance_in_words(date)
252 252 if date
253 253 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
254 254 end
255 255 end
256 256
257 257 # Renders a tree of projects as a nested set of unordered lists
258 258 # The given collection may be a subset of the whole project tree
259 259 # (eg. some intermediate nodes are private and can not be seen)
260 260 def render_project_nested_lists(projects, &block)
261 261 s = ''
262 262 if projects.any?
263 263 ancestors = []
264 264 original_project = @project
265 265 projects.sort_by(&:lft).each do |project|
266 266 # set the project environment to please macros.
267 267 @project = project
268 268 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
269 269 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
270 270 else
271 271 ancestors.pop
272 272 s << "</li>"
273 273 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
274 274 ancestors.pop
275 275 s << "</ul></li>\n"
276 276 end
277 277 end
278 278 classes = (ancestors.empty? ? 'root' : 'child')
279 279 s << "<li class='#{classes}'><div class='#{classes}'>"
280 280 s << h(block_given? ? capture(project, &block) : project.name)
281 281 s << "</div>\n"
282 282 ancestors << project
283 283 end
284 284 s << ("</li></ul>\n" * ancestors.size)
285 285 @project = original_project
286 286 end
287 287 s.html_safe
288 288 end
289 289
290 290 def render_page_hierarchy(pages, node=nil, options={})
291 291 content = ''
292 292 if pages[node]
293 293 content << "<ul class=\"pages-hierarchy\">\n"
294 294 pages[node].each do |page|
295 295 content << "<li>"
296 296 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
297 297 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
298 298 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
299 299 content << "</li>\n"
300 300 end
301 301 content << "</ul>\n"
302 302 end
303 303 content.html_safe
304 304 end
305 305
306 306 # Renders flash messages
307 307 def render_flash_messages
308 308 s = ''
309 309 flash.each do |k,v|
310 310 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
311 311 end
312 312 s.html_safe
313 313 end
314 314
315 315 # Renders tabs and their content
316 316 def render_tabs(tabs, selected=params[:tab])
317 317 if tabs.any?
318 318 unless tabs.detect {|tab| tab[:name] == selected}
319 319 selected = nil
320 320 end
321 321 selected ||= tabs.first[:name]
322 322 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
323 323 else
324 324 content_tag 'p', l(:label_no_data), :class => "nodata"
325 325 end
326 326 end
327 327
328 328 # Renders the project quick-jump box
329 329 def render_project_jump_box
330 330 return unless User.current.logged?
331 331 projects = User.current.projects.active.select(:id, :name, :identifier, :lft, :rgt).to_a
332 332 if projects.any?
333 333 options =
334 334 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
335 335 '<option value="" disabled="disabled">---</option>').html_safe
336 336
337 337 options << project_tree_options_for_select(projects, :selected => @project) do |p|
338 338 { :value => project_path(:id => p, :jump => current_menu_item) }
339 339 end
340 340
341 341 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
342 342 end
343 343 end
344 344
345 345 def project_tree_options_for_select(projects, options = {})
346 346 s = ''.html_safe
347 347 if blank_text = options[:include_blank]
348 348 if blank_text == true
349 349 blank_text = '&nbsp;'.html_safe
350 350 end
351 351 s << content_tag('option', blank_text, :value => '')
352 352 end
353 353 project_tree(projects) do |project, level|
354 354 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
355 355 tag_options = {:value => project.id}
356 356 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
357 357 tag_options[:selected] = 'selected'
358 358 else
359 359 tag_options[:selected] = nil
360 360 end
361 361 tag_options.merge!(yield(project)) if block_given?
362 362 s << content_tag('option', name_prefix + h(project), tag_options)
363 363 end
364 364 s.html_safe
365 365 end
366 366
367 367 # Yields the given block for each project with its level in the tree
368 368 #
369 369 # Wrapper for Project#project_tree
370 370 def project_tree(projects, &block)
371 371 Project.project_tree(projects, &block)
372 372 end
373 373
374 374 def principals_check_box_tags(name, principals)
375 375 s = ''
376 376 principals.each do |principal|
377 377 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
378 378 end
379 379 s.html_safe
380 380 end
381 381
382 382 # Returns a string for users/groups option tags
383 383 def principals_options_for_select(collection, selected=nil)
384 384 s = ''
385 385 if collection.include?(User.current)
386 386 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
387 387 end
388 388 groups = ''
389 389 collection.sort.each do |element|
390 390 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
391 391 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
392 392 end
393 393 unless groups.empty?
394 394 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
395 395 end
396 396 s.html_safe
397 397 end
398 398
399 399 def option_tag(name, text, value, selected=nil, options={})
400 400 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
401 401 end
402 402
403 403 def truncate_single_line_raw(string, length)
404 404 string.to_s.truncate(length).gsub(%r{[\r\n]+}m, ' ')
405 405 end
406 406
407 407 # Truncates at line break after 250 characters or options[:length]
408 408 def truncate_lines(string, options={})
409 409 length = options[:length] || 250
410 410 if string.to_s =~ /\A(.{#{length}}.*?)$/m
411 411 "#{$1}..."
412 412 else
413 413 string
414 414 end
415 415 end
416 416
417 417 def anchor(text)
418 418 text.to_s.gsub(' ', '_')
419 419 end
420 420
421 421 def html_hours(text)
422 422 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
423 423 end
424 424
425 425 def authoring(created, author, options={})
426 426 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
427 427 end
428 428
429 429 def time_tag(time)
430 430 text = distance_of_time_in_words(Time.now, time)
431 431 if @project
432 432 link_to(text, project_activity_path(@project, :from => User.current.time_to_date(time)), :title => format_time(time))
433 433 else
434 434 content_tag('abbr', text, :title => format_time(time))
435 435 end
436 436 end
437 437
438 438 def syntax_highlight_lines(name, content)
439 439 lines = []
440 440 syntax_highlight(name, content).each_line { |line| lines << line }
441 441 lines
442 442 end
443 443
444 444 def syntax_highlight(name, content)
445 445 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
446 446 end
447 447
448 448 def to_path_param(path)
449 449 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
450 450 str.blank? ? nil : str
451 451 end
452 452
453 453 def reorder_links(name, url, method = :post)
454 454 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
455 455 url.merge({"#{name}[move_to]" => 'highest'}),
456 456 :method => method, :title => l(:label_sort_highest)) +
457 457 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
458 458 url.merge({"#{name}[move_to]" => 'higher'}),
459 459 :method => method, :title => l(:label_sort_higher)) +
460 460 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
461 461 url.merge({"#{name}[move_to]" => 'lower'}),
462 462 :method => method, :title => l(:label_sort_lower)) +
463 463 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
464 464 url.merge({"#{name}[move_to]" => 'lowest'}),
465 465 :method => method, :title => l(:label_sort_lowest))
466 466 end
467 467
468 468 def breadcrumb(*args)
469 469 elements = args.flatten
470 470 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
471 471 end
472 472
473 473 def other_formats_links(&block)
474 474 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
475 475 yield Redmine::Views::OtherFormatsBuilder.new(self)
476 476 concat('</p>'.html_safe)
477 477 end
478 478
479 479 def page_header_title
480 480 if @project.nil? || @project.new_record?
481 481 h(Setting.app_title)
482 482 else
483 483 b = []
484 484 ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
485 485 if ancestors.any?
486 486 root = ancestors.shift
487 487 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
488 488 if ancestors.size > 2
489 489 b << "\xe2\x80\xa6"
490 490 ancestors = ancestors[-2, 2]
491 491 end
492 492 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
493 493 end
494 494 b << h(@project)
495 495 b.join(" \xc2\xbb ").html_safe
496 496 end
497 497 end
498 498
499 499 # Returns a h2 tag and sets the html title with the given arguments
500 500 def title(*args)
501 501 strings = args.map do |arg|
502 502 if arg.is_a?(Array) && arg.size >= 2
503 503 link_to(*arg)
504 504 else
505 505 h(arg.to_s)
506 506 end
507 507 end
508 508 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
509 509 content_tag('h2', strings.join(' &#187; ').html_safe)
510 510 end
511 511
512 512 # Sets the html title
513 513 # Returns the html title when called without arguments
514 514 # Current project name and app_title and automatically appended
515 515 # Exemples:
516 516 # html_title 'Foo', 'Bar'
517 517 # html_title # => 'Foo - Bar - My Project - Redmine'
518 518 def html_title(*args)
519 519 if args.empty?
520 520 title = @html_title || []
521 521 title << @project.name if @project
522 522 title << Setting.app_title unless Setting.app_title == title.last
523 523 title.reject(&:blank?).join(' - ')
524 524 else
525 525 @html_title ||= []
526 526 @html_title += args
527 527 end
528 528 end
529 529
530 530 # Returns the theme, controller name, and action as css classes for the
531 531 # HTML body.
532 532 def body_css_classes
533 533 css = []
534 534 if theme = Redmine::Themes.theme(Setting.ui_theme)
535 535 css << 'theme-' + theme.name
536 536 end
537 537
538 538 css << 'project-' + @project.identifier if @project && @project.identifier.present?
539 539 css << 'controller-' + controller_name
540 540 css << 'action-' + action_name
541 541 css.join(' ')
542 542 end
543 543
544 544 def accesskey(s)
545 545 @used_accesskeys ||= []
546 546 key = Redmine::AccessKeys.key_for(s)
547 547 return nil if @used_accesskeys.include?(key)
548 548 @used_accesskeys << key
549 549 key
550 550 end
551 551
552 552 # Formats text according to system settings.
553 553 # 2 ways to call this method:
554 554 # * with a String: textilizable(text, options)
555 555 # * with an object and one of its attribute: textilizable(issue, :description, options)
556 556 def textilizable(*args)
557 557 options = args.last.is_a?(Hash) ? args.pop : {}
558 558 case args.size
559 559 when 1
560 560 obj = options[:object]
561 561 text = args.shift
562 562 when 2
563 563 obj = args.shift
564 564 attr = args.shift
565 565 text = obj.send(attr).to_s
566 566 else
567 567 raise ArgumentError, 'invalid arguments to textilizable'
568 568 end
569 569 return '' if text.blank?
570 570 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
571 571 @only_path = only_path = options.delete(:only_path) == false ? false : true
572 572
573 573 text = text.dup
574 574 macros = catch_macros(text)
575 575 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
576 576
577 577 @parsed_headings = []
578 578 @heading_anchors = {}
579 579 @current_section = 0 if options[:edit_section_links]
580 580
581 581 parse_sections(text, project, obj, attr, only_path, options)
582 582 text = parse_non_pre_blocks(text, obj, macros) do |text|
583 583 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
584 584 send method_name, text, project, obj, attr, only_path, options
585 585 end
586 586 end
587 587 parse_headings(text, project, obj, attr, only_path, options)
588 588
589 589 if @parsed_headings.any?
590 590 replace_toc(text, @parsed_headings)
591 591 end
592 592
593 593 text.html_safe
594 594 end
595 595
596 596 def parse_non_pre_blocks(text, obj, macros)
597 597 s = StringScanner.new(text)
598 598 tags = []
599 599 parsed = ''
600 600 while !s.eos?
601 601 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
602 602 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
603 603 if tags.empty?
604 604 yield text
605 605 inject_macros(text, obj, macros) if macros.any?
606 606 else
607 607 inject_macros(text, obj, macros, false) if macros.any?
608 608 end
609 609 parsed << text
610 610 if tag
611 611 if closing
612 612 if tags.last.casecmp(tag) == 0
613 613 tags.pop
614 614 end
615 615 else
616 616 tags << tag.downcase
617 617 end
618 618 parsed << full_tag
619 619 end
620 620 end
621 621 # Close any non closing tags
622 622 while tag = tags.pop
623 623 parsed << "</#{tag}>"
624 624 end
625 625 parsed
626 626 end
627 627
628 628 def parse_inline_attachments(text, project, obj, attr, only_path, options)
629 629 return if options[:inline_attachments] == false
630 630
631 631 # when using an image link, try to use an attachment, if possible
632 632 attachments = options[:attachments] || []
633 633 attachments += obj.attachments if obj.respond_to?(:attachments)
634 634 if attachments.present?
635 635 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
636 636 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
637 637 # search for the picture in attachments
638 638 if found = Attachment.latest_attach(attachments, CGI.unescape(filename))
639 639 image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
640 640 desc = found.description.to_s.gsub('"', '')
641 641 if !desc.blank? && alttext.blank?
642 642 alt = " title=\"#{desc}\" alt=\"#{desc}\""
643 643 end
644 644 "src=\"#{image_url}\"#{alt}"
645 645 else
646 646 m
647 647 end
648 648 end
649 649 end
650 650 end
651 651
652 652 # Wiki links
653 653 #
654 654 # Examples:
655 655 # [[mypage]]
656 656 # [[mypage|mytext]]
657 657 # wiki links can refer other project wikis, using project name or identifier:
658 658 # [[project:]] -> wiki starting page
659 659 # [[project:|mytext]]
660 660 # [[project:mypage]]
661 661 # [[project:mypage|mytext]]
662 662 def parse_wiki_links(text, project, obj, attr, only_path, options)
663 663 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
664 664 link_project = project
665 665 esc, all, page, title = $1, $2, $3, $5
666 666 if esc.nil?
667 667 if page =~ /^([^\:]+)\:(.*)$/
668 668 identifier, page = $1, $2
669 669 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
670 670 title ||= identifier if page.blank?
671 671 end
672 672
673 673 if link_project && link_project.wiki
674 674 # extract anchor
675 675 anchor = nil
676 676 if page =~ /^(.+?)\#(.+)$/
677 677 page, anchor = $1, $2
678 678 end
679 679 anchor = sanitize_anchor_name(anchor) if anchor.present?
680 680 # check if page exists
681 681 wiki_page = link_project.wiki.find_page(page)
682 682 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
683 683 "##{anchor}"
684 684 else
685 685 case options[:wiki_links]
686 686 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
687 687 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
688 688 else
689 689 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
690 690 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
691 691 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
692 692 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
693 693 end
694 694 end
695 695 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
696 696 else
697 697 # project or wiki doesn't exist
698 698 all
699 699 end
700 700 else
701 701 all
702 702 end
703 703 end
704 704 end
705 705
706 706 # Redmine links
707 707 #
708 708 # Examples:
709 709 # Issues:
710 710 # #52 -> Link to issue #52
711 711 # Changesets:
712 712 # r52 -> Link to revision 52
713 713 # commit:a85130f -> Link to scmid starting with a85130f
714 714 # Documents:
715 715 # document#17 -> Link to document with id 17
716 716 # document:Greetings -> Link to the document with title "Greetings"
717 717 # document:"Some document" -> Link to the document with title "Some document"
718 718 # Versions:
719 719 # version#3 -> Link to version with id 3
720 720 # version:1.0.0 -> Link to version named "1.0.0"
721 721 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
722 722 # Attachments:
723 723 # attachment:file.zip -> Link to the attachment of the current object named file.zip
724 724 # Source files:
725 725 # source:some/file -> Link to the file located at /some/file in the project's repository
726 726 # source:some/file@52 -> Link to the file's revision 52
727 727 # source:some/file#L120 -> Link to line 120 of the file
728 728 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
729 729 # export:some/file -> Force the download of the file
730 730 # Forum messages:
731 731 # message#1218 -> Link to message with id 1218
732 732 # Projects:
733 733 # project:someproject -> Link to project named "someproject"
734 734 # project#3 -> Link to project with id 3
735 735 #
736 736 # Links can refer other objects from other projects, using project identifier:
737 737 # identifier:r52
738 738 # identifier:document:"Some document"
739 739 # identifier:version:1.0.0
740 740 # identifier:source:some/file
741 741 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
742 742 text.gsub!(%r{<a( [^>]+?)?>(.*?)</a>|([\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|
743 743 tag_content, leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $3, $4, $5, $6, $7, $12, $13, $10 || $14 || $20, $16 || $21, $17, $19
744 744 if tag_content
745 745 $&
746 746 else
747 747 link = nil
748 748 project = default_project
749 749 if project_identifier
750 750 project = Project.visible.find_by_identifier(project_identifier)
751 751 end
752 752 if esc.nil?
753 753 if prefix.nil? && sep == 'r'
754 754 if project
755 755 repository = nil
756 756 if repo_identifier
757 757 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
758 758 else
759 759 repository = project.repository
760 760 end
761 761 # project.changesets.visible raises an SQL error because of a double join on repositories
762 762 if repository &&
763 763 (changeset = Changeset.visible.
764 764 find_by_repository_id_and_revision(repository.id, identifier))
765 765 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
766 766 {:only_path => only_path, :controller => 'repositories',
767 767 :action => 'revision', :id => project,
768 768 :repository_id => repository.identifier_param,
769 769 :rev => changeset.revision},
770 770 :class => 'changeset',
771 771 :title => truncate_single_line_raw(changeset.comments, 100))
772 772 end
773 773 end
774 774 elsif sep == '#'
775 775 oid = identifier.to_i
776 776 case prefix
777 777 when nil
778 778 if oid.to_s == identifier &&
779 779 issue = Issue.visible.find_by_id(oid)
780 780 anchor = comment_id ? "note-#{comment_id}" : nil
781 781 link = link_to("##{oid}#{comment_suffix}",
782 782 issue_url(issue, :only_path => only_path, :anchor => anchor),
783 783 :class => issue.css_classes,
784 :title => "#{issue.subject.truncate(100)} (#{issue.status.name})")
784 :title => "#{issue.tracker.name}: #{issue.subject.truncate(100)} (#{issue.status.name})")
785 785 end
786 786 when 'document'
787 787 if document = Document.visible.find_by_id(oid)
788 788 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
789 789 end
790 790 when 'version'
791 791 if version = Version.visible.find_by_id(oid)
792 792 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
793 793 end
794 794 when 'message'
795 795 if message = Message.visible.find_by_id(oid)
796 796 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
797 797 end
798 798 when 'forum'
799 799 if board = Board.visible.find_by_id(oid)
800 800 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
801 801 end
802 802 when 'news'
803 803 if news = News.visible.find_by_id(oid)
804 804 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
805 805 end
806 806 when 'project'
807 807 if p = Project.visible.find_by_id(oid)
808 808 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
809 809 end
810 810 end
811 811 elsif sep == ':'
812 812 # removes the double quotes if any
813 813 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
814 814 name = CGI.unescapeHTML(name)
815 815 case prefix
816 816 when 'document'
817 817 if project && document = project.documents.visible.find_by_title(name)
818 818 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
819 819 end
820 820 when 'version'
821 821 if project && version = project.versions.visible.find_by_name(name)
822 822 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
823 823 end
824 824 when 'forum'
825 825 if project && board = project.boards.visible.find_by_name(name)
826 826 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
827 827 end
828 828 when 'news'
829 829 if project && news = project.news.visible.find_by_title(name)
830 830 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
831 831 end
832 832 when 'commit', 'source', 'export'
833 833 if project
834 834 repository = nil
835 835 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
836 836 repo_prefix, repo_identifier, name = $1, $2, $3
837 837 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
838 838 else
839 839 repository = project.repository
840 840 end
841 841 if prefix == 'commit'
842 842 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
843 843 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},
844 844 :class => 'changeset',
845 845 :title => truncate_single_line_raw(changeset.comments, 100)
846 846 end
847 847 else
848 848 if repository && User.current.allowed_to?(:browse_repository, project)
849 849 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
850 850 path, rev, anchor = $1, $3, $5
851 851 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
852 852 :path => to_path_param(path),
853 853 :rev => rev,
854 854 :anchor => anchor},
855 855 :class => (prefix == 'export' ? 'source download' : 'source')
856 856 end
857 857 end
858 858 repo_prefix = nil
859 859 end
860 860 when 'attachment'
861 861 attachments = options[:attachments] || []
862 862 attachments += obj.attachments if obj.respond_to?(:attachments)
863 863 if attachments && attachment = Attachment.latest_attach(attachments, name)
864 864 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
865 865 end
866 866 when 'project'
867 867 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
868 868 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
869 869 end
870 870 end
871 871 end
872 872 end
873 873 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
874 874 end
875 875 end
876 876 end
877 877
878 878 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
879 879
880 880 def parse_sections(text, project, obj, attr, only_path, options)
881 881 return unless options[:edit_section_links]
882 882 text.gsub!(HEADING_RE) do
883 883 heading = $1
884 884 @current_section += 1
885 885 if @current_section > 1
886 886 content_tag('div',
887 887 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
888 888 :class => 'contextual',
889 889 :title => l(:button_edit_section),
890 890 :id => "section-#{@current_section}") + heading.html_safe
891 891 else
892 892 heading
893 893 end
894 894 end
895 895 end
896 896
897 897 # Headings and TOC
898 898 # Adds ids and links to headings unless options[:headings] is set to false
899 899 def parse_headings(text, project, obj, attr, only_path, options)
900 900 return if options[:headings] == false
901 901
902 902 text.gsub!(HEADING_RE) do
903 903 level, attrs, content = $2.to_i, $3, $4
904 904 item = strip_tags(content).strip
905 905 anchor = sanitize_anchor_name(item)
906 906 # used for single-file wiki export
907 907 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
908 908 @heading_anchors[anchor] ||= 0
909 909 idx = (@heading_anchors[anchor] += 1)
910 910 if idx > 1
911 911 anchor = "#{anchor}-#{idx}"
912 912 end
913 913 @parsed_headings << [level, anchor, item]
914 914 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
915 915 end
916 916 end
917 917
918 918 MACROS_RE = /(
919 919 (!)? # escaping
920 920 (
921 921 \{\{ # opening tag
922 922 ([\w]+) # macro name
923 923 (\(([^\n\r]*?)\))? # optional arguments
924 924 ([\n\r].*?[\n\r])? # optional block of text
925 925 \}\} # closing tag
926 926 )
927 927 )/mx unless const_defined?(:MACROS_RE)
928 928
929 929 MACRO_SUB_RE = /(
930 930 \{\{
931 931 macro\((\d+)\)
932 932 \}\}
933 933 )/x unless const_defined?(:MACRO_SUB_RE)
934 934
935 935 # Extracts macros from text
936 936 def catch_macros(text)
937 937 macros = {}
938 938 text.gsub!(MACROS_RE) do
939 939 all, macro = $1, $4.downcase
940 940 if macro_exists?(macro) || all =~ MACRO_SUB_RE
941 941 index = macros.size
942 942 macros[index] = all
943 943 "{{macro(#{index})}}"
944 944 else
945 945 all
946 946 end
947 947 end
948 948 macros
949 949 end
950 950
951 951 # Executes and replaces macros in text
952 952 def inject_macros(text, obj, macros, execute=true)
953 953 text.gsub!(MACRO_SUB_RE) do
954 954 all, index = $1, $2.to_i
955 955 orig = macros.delete(index)
956 956 if execute && orig && orig =~ MACROS_RE
957 957 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
958 958 if esc.nil?
959 959 h(exec_macro(macro, obj, args, block) || all)
960 960 else
961 961 h(all)
962 962 end
963 963 elsif orig
964 964 h(orig)
965 965 else
966 966 h(all)
967 967 end
968 968 end
969 969 end
970 970
971 971 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
972 972
973 973 # Renders the TOC with given headings
974 974 def replace_toc(text, headings)
975 975 text.gsub!(TOC_RE) do
976 976 left_align, right_align = $2, $3
977 977 # Keep only the 4 first levels
978 978 headings = headings.select{|level, anchor, item| level <= 4}
979 979 if headings.empty?
980 980 ''
981 981 else
982 982 div_class = 'toc'
983 983 div_class << ' right' if right_align
984 984 div_class << ' left' if left_align
985 985 out = "<ul class=\"#{div_class}\"><li>"
986 986 root = headings.map(&:first).min
987 987 current = root
988 988 started = false
989 989 headings.each do |level, anchor, item|
990 990 if level > current
991 991 out << '<ul><li>' * (level - current)
992 992 elsif level < current
993 993 out << "</li></ul>\n" * (current - level) + "</li><li>"
994 994 elsif started
995 995 out << '</li><li>'
996 996 end
997 997 out << "<a href=\"##{anchor}\">#{item}</a>"
998 998 current = level
999 999 started = true
1000 1000 end
1001 1001 out << '</li></ul>' * (current - root)
1002 1002 out << '</li></ul>'
1003 1003 end
1004 1004 end
1005 1005 end
1006 1006
1007 1007 # Same as Rails' simple_format helper without using paragraphs
1008 1008 def simple_format_without_paragraph(text)
1009 1009 text.to_s.
1010 1010 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1011 1011 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1012 1012 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1013 1013 html_safe
1014 1014 end
1015 1015
1016 1016 def lang_options_for_select(blank=true)
1017 1017 (blank ? [["(auto)", ""]] : []) + languages_options
1018 1018 end
1019 1019
1020 1020 def labelled_form_for(*args, &proc)
1021 1021 args << {} unless args.last.is_a?(Hash)
1022 1022 options = args.last
1023 1023 if args.first.is_a?(Symbol)
1024 1024 options.merge!(:as => args.shift)
1025 1025 end
1026 1026 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1027 1027 form_for(*args, &proc)
1028 1028 end
1029 1029
1030 1030 def labelled_fields_for(*args, &proc)
1031 1031 args << {} unless args.last.is_a?(Hash)
1032 1032 options = args.last
1033 1033 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1034 1034 fields_for(*args, &proc)
1035 1035 end
1036 1036
1037 1037 def error_messages_for(*objects)
1038 1038 html = ""
1039 1039 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1040 1040 errors = objects.map {|o| o.errors.full_messages}.flatten
1041 1041 if errors.any?
1042 1042 html << "<div id='errorExplanation'><ul>\n"
1043 1043 errors.each do |error|
1044 1044 html << "<li>#{h error}</li>\n"
1045 1045 end
1046 1046 html << "</ul></div>\n"
1047 1047 end
1048 1048 html.html_safe
1049 1049 end
1050 1050
1051 1051 def delete_link(url, options={})
1052 1052 options = {
1053 1053 :method => :delete,
1054 1054 :data => {:confirm => l(:text_are_you_sure)},
1055 1055 :class => 'icon icon-del'
1056 1056 }.merge(options)
1057 1057
1058 1058 link_to l(:button_delete), url, options
1059 1059 end
1060 1060
1061 1061 def preview_link(url, form, target='preview', options={})
1062 1062 content_tag 'a', l(:label_preview), {
1063 1063 :href => "#",
1064 1064 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1065 1065 :accesskey => accesskey(:preview)
1066 1066 }.merge(options)
1067 1067 end
1068 1068
1069 1069 def link_to_function(name, function, html_options={})
1070 1070 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1071 1071 end
1072 1072
1073 1073 # Helper to render JSON in views
1074 1074 def raw_json(arg)
1075 1075 arg.to_json.to_s.gsub('/', '\/').html_safe
1076 1076 end
1077 1077
1078 1078 def back_url
1079 1079 url = params[:back_url]
1080 1080 if url.nil? && referer = request.env['HTTP_REFERER']
1081 1081 url = CGI.unescape(referer.to_s)
1082 1082 end
1083 1083 url
1084 1084 end
1085 1085
1086 1086 def back_url_hidden_field_tag
1087 1087 url = back_url
1088 1088 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1089 1089 end
1090 1090
1091 1091 def check_all_links(form_name)
1092 1092 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1093 1093 " | ".html_safe +
1094 1094 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1095 1095 end
1096 1096
1097 1097 def toggle_checkboxes_link(selector)
1098 1098 link_to_function image_tag('toggle_check.png'),
1099 1099 "toggleCheckboxesBySelector('#{selector}')",
1100 1100 :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}"
1101 1101 end
1102 1102
1103 1103 def progress_bar(pcts, options={})
1104 1104 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1105 1105 pcts = pcts.collect(&:round)
1106 1106 pcts[1] = pcts[1] - pcts[0]
1107 1107 pcts << (100 - pcts[1] - pcts[0])
1108 1108 width = options[:width] || '100px;'
1109 1109 legend = options[:legend] || ''
1110 1110 content_tag('table',
1111 1111 content_tag('tr',
1112 1112 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1113 1113 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1114 1114 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1115 1115 ), :class => "progress progress-#{pcts[0]}", :style => "width: #{width};").html_safe +
1116 1116 content_tag('p', legend, :class => 'percent').html_safe
1117 1117 end
1118 1118
1119 1119 def checked_image(checked=true)
1120 1120 if checked
1121 1121 @checked_image_tag ||= image_tag('toggle_check.png')
1122 1122 end
1123 1123 end
1124 1124
1125 1125 def context_menu(url)
1126 1126 unless @context_menu_included
1127 1127 content_for :header_tags do
1128 1128 javascript_include_tag('context_menu') +
1129 1129 stylesheet_link_tag('context_menu')
1130 1130 end
1131 1131 if l(:direction) == 'rtl'
1132 1132 content_for :header_tags do
1133 1133 stylesheet_link_tag('context_menu_rtl')
1134 1134 end
1135 1135 end
1136 1136 @context_menu_included = true
1137 1137 end
1138 1138 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1139 1139 end
1140 1140
1141 1141 def calendar_for(field_id)
1142 1142 include_calendar_headers_tags
1143 1143 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1144 1144 end
1145 1145
1146 1146 def include_calendar_headers_tags
1147 1147 unless @calendar_headers_tags_included
1148 1148 tags = ''.html_safe
1149 1149 @calendar_headers_tags_included = true
1150 1150 content_for :header_tags do
1151 1151 start_of_week = Setting.start_of_week
1152 1152 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1153 1153 # Redmine uses 1..7 (monday..sunday) in settings and locales
1154 1154 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1155 1155 start_of_week = start_of_week.to_i % 7
1156 1156 tags << javascript_tag(
1157 1157 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1158 1158 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1159 1159 path_to_image('/images/calendar.png') +
1160 1160 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1161 1161 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1162 1162 "beforeShow: beforeShowDatePicker};")
1163 1163 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1164 1164 unless jquery_locale == 'en'
1165 1165 tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
1166 1166 end
1167 1167 tags
1168 1168 end
1169 1169 end
1170 1170 end
1171 1171
1172 1172 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1173 1173 # Examples:
1174 1174 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1175 1175 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1176 1176 #
1177 1177 def stylesheet_link_tag(*sources)
1178 1178 options = sources.last.is_a?(Hash) ? sources.pop : {}
1179 1179 plugin = options.delete(:plugin)
1180 1180 sources = sources.map do |source|
1181 1181 if plugin
1182 1182 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1183 1183 elsif current_theme && current_theme.stylesheets.include?(source)
1184 1184 current_theme.stylesheet_path(source)
1185 1185 else
1186 1186 source
1187 1187 end
1188 1188 end
1189 1189 super *sources, options
1190 1190 end
1191 1191
1192 1192 # Overrides Rails' image_tag with themes and plugins support.
1193 1193 # Examples:
1194 1194 # image_tag('image.png') # => picks image.png from the current theme or defaults
1195 1195 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1196 1196 #
1197 1197 def image_tag(source, options={})
1198 1198 if plugin = options.delete(:plugin)
1199 1199 source = "/plugin_assets/#{plugin}/images/#{source}"
1200 1200 elsif current_theme && current_theme.images.include?(source)
1201 1201 source = current_theme.image_path(source)
1202 1202 end
1203 1203 super source, options
1204 1204 end
1205 1205
1206 1206 # Overrides Rails' javascript_include_tag with plugins support
1207 1207 # Examples:
1208 1208 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1209 1209 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1210 1210 #
1211 1211 def javascript_include_tag(*sources)
1212 1212 options = sources.last.is_a?(Hash) ? sources.pop : {}
1213 1213 if plugin = options.delete(:plugin)
1214 1214 sources = sources.map do |source|
1215 1215 if plugin
1216 1216 "/plugin_assets/#{plugin}/javascripts/#{source}"
1217 1217 else
1218 1218 source
1219 1219 end
1220 1220 end
1221 1221 end
1222 1222 super *sources, options
1223 1223 end
1224 1224
1225 1225 def sidebar_content?
1226 1226 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1227 1227 end
1228 1228
1229 1229 def view_layouts_base_sidebar_hook_response
1230 1230 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1231 1231 end
1232 1232
1233 1233 def email_delivery_enabled?
1234 1234 !!ActionMailer::Base.perform_deliveries
1235 1235 end
1236 1236
1237 1237 # Returns the avatar image tag for the given +user+ if avatars are enabled
1238 1238 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1239 1239 def avatar(user, options = { })
1240 1240 if Setting.gravatar_enabled?
1241 1241 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1242 1242 email = nil
1243 1243 if user.respond_to?(:mail)
1244 1244 email = user.mail
1245 1245 elsif user.to_s =~ %r{<(.+?)>}
1246 1246 email = $1
1247 1247 end
1248 1248 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1249 1249 else
1250 1250 ''
1251 1251 end
1252 1252 end
1253 1253
1254 1254 # Returns a link to edit user's avatar if avatars are enabled
1255 1255 def avatar_edit_link(user, options={})
1256 1256 if Setting.gravatar_enabled?
1257 1257 url = "https://gravatar.com"
1258 1258 link_to avatar(user, {:title => l(:button_edit)}.merge(options)), url, :target => '_blank'
1259 1259 end
1260 1260 end
1261 1261
1262 1262 def sanitize_anchor_name(anchor)
1263 1263 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1264 1264 end
1265 1265
1266 1266 # Returns the javascript tags that are included in the html layout head
1267 1267 def javascript_heads
1268 1268 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.3', 'application')
1269 1269 unless User.current.pref.warn_on_leaving_unsaved == '0'
1270 1270 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1271 1271 end
1272 1272 tags
1273 1273 end
1274 1274
1275 1275 def favicon
1276 1276 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1277 1277 end
1278 1278
1279 1279 # Returns the path to the favicon
1280 1280 def favicon_path
1281 1281 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1282 1282 image_path(icon)
1283 1283 end
1284 1284
1285 1285 # Returns the full URL to the favicon
1286 1286 def favicon_url
1287 1287 # TODO: use #image_url introduced in Rails4
1288 1288 path = favicon_path
1289 1289 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1290 1290 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1291 1291 end
1292 1292
1293 1293 def robot_exclusion_tag
1294 1294 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1295 1295 end
1296 1296
1297 1297 # Returns true if arg is expected in the API response
1298 1298 def include_in_api_response?(arg)
1299 1299 unless @included_in_api_response
1300 1300 param = params[:include]
1301 1301 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1302 1302 @included_in_api_response.collect!(&:strip)
1303 1303 end
1304 1304 @included_in_api_response.include?(arg.to_s)
1305 1305 end
1306 1306
1307 1307 # Returns options or nil if nometa param or X-Redmine-Nometa header
1308 1308 # was set in the request
1309 1309 def api_meta(options)
1310 1310 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1311 1311 # compatibility mode for activeresource clients that raise
1312 1312 # an error when deserializing an array with attributes
1313 1313 nil
1314 1314 else
1315 1315 options
1316 1316 end
1317 1317 end
1318 1318
1319 1319 def generate_csv(&block)
1320 1320 decimal_separator = l(:general_csv_decimal_separator)
1321 1321 encoding = l(:general_csv_encoding)
1322 1322 end
1323 1323
1324 1324 private
1325 1325
1326 1326 def wiki_helper
1327 1327 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1328 1328 extend helper
1329 1329 return self
1330 1330 end
1331 1331
1332 1332 def link_to_content_update(text, url_params = {}, html_options = {})
1333 1333 link_to(text, url_params, html_options)
1334 1334 end
1335 1335 end
@@ -1,1528 +1,1528
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2015 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 File.expand_path('../../../test_helper', __FILE__)
21 21
22 22 class ApplicationHelperTest < ActionView::TestCase
23 23 include Redmine::I18n
24 24 include ERB::Util
25 25 include Rails.application.routes.url_helpers
26 26
27 27 fixtures :projects, :roles, :enabled_modules, :users,
28 28 :email_addresses,
29 29 :repositories, :changesets,
30 30 :projects_trackers,
31 31 :trackers, :issue_statuses, :issues, :versions, :documents,
32 32 :wikis, :wiki_pages, :wiki_contents,
33 33 :boards, :messages, :news,
34 34 :attachments, :enumerations
35 35
36 36 def setup
37 37 super
38 38 set_tmp_attachments_directory
39 39 @russian_test = "\xd1\x82\xd0\xb5\xd1\x81\xd1\x82".force_encoding('UTF-8')
40 40 end
41 41
42 42 test "#link_to_if_authorized for authorized user should allow using the :controller and :action for the target link" do
43 43 User.current = User.find_by_login('admin')
44 44
45 45 @project = Issue.first.project # Used by helper
46 46 response = link_to_if_authorized('By controller/actionr',
47 47 {:controller => 'issues', :action => 'edit', :id => Issue.first.id})
48 48 assert_match /href/, response
49 49 end
50 50
51 51 test "#link_to_if_authorized for unauthorized user should display nothing if user isn't authorized" do
52 52 User.current = User.find_by_login('dlopper')
53 53 @project = Project.find('private-child')
54 54 issue = @project.issues.first
55 55 assert !issue.visible?
56 56
57 57 response = link_to_if_authorized('Never displayed',
58 58 {:controller => 'issues', :action => 'show', :id => issue})
59 59 assert_nil response
60 60 end
61 61
62 62 def test_auto_links
63 63 to_test = {
64 64 'http://foo.bar' => '<a class="external" href="http://foo.bar">http://foo.bar</a>',
65 65 'http://foo.bar/~user' => '<a class="external" href="http://foo.bar/~user">http://foo.bar/~user</a>',
66 66 'http://foo.bar.' => '<a class="external" href="http://foo.bar">http://foo.bar</a>.',
67 67 'https://foo.bar.' => '<a class="external" href="https://foo.bar">https://foo.bar</a>.',
68 68 'This is a link: http://foo.bar.' => 'This is a link: <a class="external" href="http://foo.bar">http://foo.bar</a>.',
69 69 'A link (eg. http://foo.bar).' => 'A link (eg. <a class="external" href="http://foo.bar">http://foo.bar</a>).',
70 70 'http://foo.bar/foo.bar#foo.bar.' => '<a class="external" href="http://foo.bar/foo.bar#foo.bar">http://foo.bar/foo.bar#foo.bar</a>.',
71 71 'http://www.foo.bar/Test_(foobar)' => '<a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>',
72 72 '(see inline link : http://www.foo.bar/Test_(foobar))' => '(see inline link : <a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>)',
73 73 '(see inline link : http://www.foo.bar/Test)' => '(see inline link : <a class="external" href="http://www.foo.bar/Test">http://www.foo.bar/Test</a>)',
74 74 '(see inline link : http://www.foo.bar/Test).' => '(see inline link : <a class="external" href="http://www.foo.bar/Test">http://www.foo.bar/Test</a>).',
75 75 '(see "inline link":http://www.foo.bar/Test_(foobar))' => '(see <a href="http://www.foo.bar/Test_(foobar)" class="external">inline link</a>)',
76 76 '(see "inline link":http://www.foo.bar/Test)' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>)',
77 77 '(see "inline link":http://www.foo.bar/Test).' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>).',
78 78 'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>',
79 79 'http://foo.bar/page?p=1&t=z&s=' => '<a class="external" href="http://foo.bar/page?p=1&#38;t=z&#38;s=">http://foo.bar/page?p=1&#38;t=z&#38;s=</a>',
80 80 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>',
81 81 'http://foo@www.bar.com' => '<a class="external" href="http://foo@www.bar.com">http://foo@www.bar.com</a>',
82 82 'http://foo:bar@www.bar.com' => '<a class="external" href="http://foo:bar@www.bar.com">http://foo:bar@www.bar.com</a>',
83 83 'ftp://foo.bar' => '<a class="external" href="ftp://foo.bar">ftp://foo.bar</a>',
84 84 'ftps://foo.bar' => '<a class="external" href="ftps://foo.bar">ftps://foo.bar</a>',
85 85 'sftp://foo.bar' => '<a class="external" href="sftp://foo.bar">sftp://foo.bar</a>',
86 86 # two exclamation marks
87 87 'http://example.net/path!602815048C7B5C20!302.html' => '<a class="external" href="http://example.net/path!602815048C7B5C20!302.html">http://example.net/path!602815048C7B5C20!302.html</a>',
88 88 # escaping
89 89 'http://foo"bar' => '<a class="external" href="http://foo&quot;bar">http://foo&quot;bar</a>',
90 90 # wrap in angle brackets
91 91 '<http://foo.bar>' => '&lt;<a class="external" href="http://foo.bar">http://foo.bar</a>&gt;',
92 92 # invalid urls
93 93 'http://' => 'http://',
94 94 'www.' => 'www.',
95 95 'test-www.bar.com' => 'test-www.bar.com',
96 96 }
97 97 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
98 98 end
99 99
100 100 def test_auto_links_with_non_ascii_characters
101 101 to_test = {
102 102 "http://foo.bar/#{@russian_test}" =>
103 103 %|<a class="external" href="http://foo.bar/#{@russian_test}">http://foo.bar/#{@russian_test}</a>|
104 104 }
105 105 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
106 106 end
107 107
108 108 def test_auto_mailto
109 109 to_test = {
110 110 'test@foo.bar' => '<a class="email" href="mailto:test@foo.bar">test@foo.bar</a>',
111 111 'test@www.foo.bar' => '<a class="email" href="mailto:test@www.foo.bar">test@www.foo.bar</a>',
112 112 }
113 113 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
114 114 end
115 115
116 116 def test_inline_images
117 117 to_test = {
118 118 '!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
119 119 'floating !>http://foo.bar/image.jpg!' => 'floating <div style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></div>',
120 120 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class <img src="http://foo.bar/image.jpg" class="some-class" alt="" />',
121 121 'with style !{width:100px;height:100px}http://foo.bar/image.jpg!' => 'with style <img src="http://foo.bar/image.jpg" style="width:100px;height:100px;" alt="" />',
122 122 'with title !http://foo.bar/image.jpg(This is a title)!' => 'with title <img src="http://foo.bar/image.jpg" title="This is a title" alt="This is a title" />',
123 123 'with title !http://foo.bar/image.jpg(This is a double-quoted "title")!' => 'with title <img src="http://foo.bar/image.jpg" title="This is a double-quoted &quot;title&quot;" alt="This is a double-quoted &quot;title&quot;" />',
124 124 }
125 125 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
126 126 end
127 127
128 128 def test_inline_images_inside_tags
129 129 raw = <<-RAW
130 130 h1. !foo.png! Heading
131 131
132 132 Centered image:
133 133
134 134 p=. !bar.gif!
135 135 RAW
136 136
137 137 assert textilizable(raw).include?('<img src="foo.png" alt="" />')
138 138 assert textilizable(raw).include?('<img src="bar.gif" alt="" />')
139 139 end
140 140
141 141 def test_attached_images
142 142 to_test = {
143 143 'Inline image: !logo.gif!' => 'Inline image: <img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" />',
144 144 'Inline image: !logo.GIF!' => 'Inline image: <img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" />',
145 145 'No match: !ogo.gif!' => 'No match: <img src="ogo.gif" alt="" />',
146 146 'No match: !ogo.GIF!' => 'No match: <img src="ogo.GIF" alt="" />',
147 147 # link image
148 148 '!logo.gif!:http://foo.bar/' => '<a href="http://foo.bar/"><img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" /></a>',
149 149 }
150 150 attachments = Attachment.all
151 151 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
152 152 end
153 153
154 154 def test_attached_images_with_textile_and_non_ascii_filename
155 155 attachment = Attachment.generate!(:filename => 'cafΓ©.jpg')
156 156 with_settings :text_formatting => 'textile' do
157 157 assert_include %(<img src="/attachments/download/#{attachment.id}/caf%C3%A9.jpg" alt="" />),
158 158 textilizable("!cafΓ©.jpg!)", :attachments => [attachment])
159 159 end
160 160 end
161 161
162 162 def test_attached_images_with_markdown_and_non_ascii_filename
163 163 skip unless Object.const_defined?(:Redcarpet)
164 164
165 165 attachment = Attachment.generate!(:filename => 'cafΓ©.jpg')
166 166 with_settings :text_formatting => 'markdown' do
167 167 assert_include %(<img src="/attachments/download/#{attachment.id}/caf%C3%A9.jpg" alt="">),
168 168 textilizable("![](cafΓ©.jpg)", :attachments => [attachment])
169 169 end
170 170 end
171 171
172 172 def test_attached_images_filename_extension
173 173 set_tmp_attachments_directory
174 174 a1 = Attachment.new(
175 175 :container => Issue.find(1),
176 176 :file => mock_file_with_options({:original_filename => "testtest.JPG"}),
177 177 :author => User.find(1))
178 178 assert a1.save
179 179 assert_equal "testtest.JPG", a1.filename
180 180 assert_equal "image/jpeg", a1.content_type
181 181 assert a1.image?
182 182
183 183 a2 = Attachment.new(
184 184 :container => Issue.find(1),
185 185 :file => mock_file_with_options({:original_filename => "testtest.jpeg"}),
186 186 :author => User.find(1))
187 187 assert a2.save
188 188 assert_equal "testtest.jpeg", a2.filename
189 189 assert_equal "image/jpeg", a2.content_type
190 190 assert a2.image?
191 191
192 192 a3 = Attachment.new(
193 193 :container => Issue.find(1),
194 194 :file => mock_file_with_options({:original_filename => "testtest.JPE"}),
195 195 :author => User.find(1))
196 196 assert a3.save
197 197 assert_equal "testtest.JPE", a3.filename
198 198 assert_equal "image/jpeg", a3.content_type
199 199 assert a3.image?
200 200
201 201 a4 = Attachment.new(
202 202 :container => Issue.find(1),
203 203 :file => mock_file_with_options({:original_filename => "Testtest.BMP"}),
204 204 :author => User.find(1))
205 205 assert a4.save
206 206 assert_equal "Testtest.BMP", a4.filename
207 207 assert_equal "image/x-ms-bmp", a4.content_type
208 208 assert a4.image?
209 209
210 210 to_test = {
211 211 'Inline image: !testtest.jpg!' =>
212 212 'Inline image: <img src="/attachments/download/' + a1.id.to_s + '/testtest.JPG" alt="" />',
213 213 'Inline image: !testtest.jpeg!' =>
214 214 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testtest.jpeg" alt="" />',
215 215 'Inline image: !testtest.jpe!' =>
216 216 'Inline image: <img src="/attachments/download/' + a3.id.to_s + '/testtest.JPE" alt="" />',
217 217 'Inline image: !testtest.bmp!' =>
218 218 'Inline image: <img src="/attachments/download/' + a4.id.to_s + '/Testtest.BMP" alt="" />',
219 219 }
220 220
221 221 attachments = [a1, a2, a3, a4]
222 222 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
223 223 end
224 224
225 225 def test_attached_images_should_read_later
226 226 set_fixtures_attachments_directory
227 227 a1 = Attachment.find(16)
228 228 assert_equal "testfile.png", a1.filename
229 229 assert a1.readable?
230 230 assert (! a1.visible?(User.anonymous))
231 231 assert a1.visible?(User.find(2))
232 232 a2 = Attachment.find(17)
233 233 assert_equal "testfile.PNG", a2.filename
234 234 assert a2.readable?
235 235 assert (! a2.visible?(User.anonymous))
236 236 assert a2.visible?(User.find(2))
237 237 assert a1.created_on < a2.created_on
238 238
239 239 to_test = {
240 240 'Inline image: !testfile.png!' =>
241 241 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testfile.PNG" alt="" />',
242 242 'Inline image: !Testfile.PNG!' =>
243 243 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testfile.PNG" alt="" />',
244 244 }
245 245 attachments = [a1, a2]
246 246 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
247 247 set_tmp_attachments_directory
248 248 end
249 249
250 250 def test_textile_external_links
251 251 to_test = {
252 252 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>',
253 253 'This is an intern "link":/foo/bar' => 'This is an intern <a href="/foo/bar">link</a>',
254 254 '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>',
255 255 '"link (Link title with "double-quotes")":http://foo.bar' => '<a href="http://foo.bar" title="Link title with &quot;double-quotes&quot;" class="external">link</a>',
256 256 "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":</p>\n\n\n\t<p>Another paragraph",
257 257 # no multiline link text
258 258 "This is a double quote \"on the first line\nand another on a second line\":test" => "This is a double quote \"on the first line<br />and another on a second line\":test",
259 259 # mailto link
260 260 "\"system administrator\":mailto:sysadmin@example.com?subject=redmine%20permissions" => "<a href=\"mailto:sysadmin@example.com?subject=redmine%20permissions\">system administrator</a>",
261 261 # two exclamation marks
262 262 '"a link":http://example.net/path!602815048C7B5C20!302.html' => '<a href="http://example.net/path!602815048C7B5C20!302.html" class="external">a link</a>',
263 263 # escaping
264 264 '"test":http://foo"bar' => '<a href="http://foo&quot;bar" class="external">test</a>',
265 265 }
266 266 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
267 267 end
268 268
269 269 def test_textile_external_links_with_non_ascii_characters
270 270 to_test = {
271 271 %|This is a "link":http://foo.bar/#{@russian_test}| =>
272 272 %|This is a <a href="http://foo.bar/#{@russian_test}" class="external">link</a>|
273 273 }
274 274 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
275 275 end
276 276
277 277 def test_redmine_links
278 278 issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3},
279 :class => Issue.find(3).css_classes, :title => 'Error 281 when updating a recipe (New)')
279 :class => Issue.find(3).css_classes, :title => 'Bug: Error 281 when updating a recipe (New)')
280 280 note_link = link_to('#3-14', {:controller => 'issues', :action => 'show', :id => 3, :anchor => 'note-14'},
281 :class => Issue.find(3).css_classes, :title => 'Error 281 when updating a recipe (New)')
281 :class => Issue.find(3).css_classes, :title => 'Bug: Error 281 when updating a recipe (New)')
282 282 note_link2 = link_to('#3#note-14', {:controller => 'issues', :action => 'show', :id => 3, :anchor => 'note-14'},
283 :class => Issue.find(3).css_classes, :title => 'Error 281 when updating a recipe (New)')
283 :class => Issue.find(3).css_classes, :title => 'Bug: Error 281 when updating a recipe (New)')
284 284
285 285 revision_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
286 286 :class => 'changeset', :title => 'My very first commit do not escaping #<>&')
287 287 revision_link2 = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
288 288 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
289 289
290 290 changeset_link2 = link_to('691322a8eb01e11fd7',
291 291 {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
292 292 :class => 'changeset', :title => 'My very first commit do not escaping #<>&')
293 293
294 294 document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
295 295 :class => 'document')
296 296
297 297 version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
298 298 :class => 'version')
299 299
300 300 board_url = {:controller => 'boards', :action => 'show', :id => 2, :project_id => 'ecookbook'}
301 301
302 302 message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4}
303 303
304 304 news_url = {:controller => 'news', :action => 'show', :id => 1}
305 305
306 306 project_url = {:controller => 'projects', :action => 'show', :id => 'subproject1'}
307 307
308 308 source_url = '/projects/ecookbook/repository/entry/some/file'
309 309 source_url_with_rev = '/projects/ecookbook/repository/revisions/52/entry/some/file'
310 310 source_url_with_ext = '/projects/ecookbook/repository/entry/some/file.ext'
311 311 source_url_with_rev_and_ext = '/projects/ecookbook/repository/revisions/52/entry/some/file.ext'
312 312 source_url_with_branch = '/projects/ecookbook/repository/revisions/branch/entry/some/file'
313 313
314 314 export_url = '/projects/ecookbook/repository/raw/some/file'
315 315 export_url_with_rev = '/projects/ecookbook/repository/revisions/52/raw/some/file'
316 316 export_url_with_ext = '/projects/ecookbook/repository/raw/some/file.ext'
317 317 export_url_with_rev_and_ext = '/projects/ecookbook/repository/revisions/52/raw/some/file.ext'
318 318 export_url_with_branch = '/projects/ecookbook/repository/revisions/branch/raw/some/file'
319 319
320 320 to_test = {
321 321 # tickets
322 322 '#3, [#3], (#3) and #3.' => "#{issue_link}, [#{issue_link}], (#{issue_link}) and #{issue_link}.",
323 323 # ticket notes
324 324 '#3-14' => note_link,
325 325 '#3#note-14' => note_link2,
326 326 # should not ignore leading zero
327 327 '#03' => '#03',
328 328 # changesets
329 329 'r1' => revision_link,
330 330 'r1.' => "#{revision_link}.",
331 331 'r1, r2' => "#{revision_link}, #{revision_link2}",
332 332 'r1,r2' => "#{revision_link},#{revision_link2}",
333 333 'commit:691322a8eb01e11fd7' => changeset_link2,
334 334 # documents
335 335 'document#1' => document_link,
336 336 'document:"Test document"' => document_link,
337 337 # versions
338 338 'version#2' => version_link,
339 339 'version:1.0' => version_link,
340 340 'version:"1.0"' => version_link,
341 341 # source
342 342 'source:some/file' => link_to('source:some/file', source_url, :class => 'source'),
343 343 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
344 344 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".",
345 345 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
346 346 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".",
347 347 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
348 348 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",",
349 349 'source:/some/file@52' => link_to('source:/some/file@52', source_url_with_rev, :class => 'source'),
350 350 'source:/some/file@branch' => link_to('source:/some/file@branch', source_url_with_branch, :class => 'source'),
351 351 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_rev_and_ext, :class => 'source'),
352 352 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url + "#L110", :class => 'source'),
353 353 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext + "#L110", :class => 'source'),
354 354 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url_with_rev + "#L110", :class => 'source'),
355 355 # export
356 356 'export:/some/file' => link_to('export:/some/file', export_url, :class => 'source download'),
357 357 'export:/some/file.ext' => link_to('export:/some/file.ext', export_url_with_ext, :class => 'source download'),
358 358 'export:/some/file@52' => link_to('export:/some/file@52', export_url_with_rev, :class => 'source download'),
359 359 'export:/some/file.ext@52' => link_to('export:/some/file.ext@52', export_url_with_rev_and_ext, :class => 'source download'),
360 360 'export:/some/file@branch' => link_to('export:/some/file@branch', export_url_with_branch, :class => 'source download'),
361 361 # forum
362 362 'forum#2' => link_to('Discussion', board_url, :class => 'board'),
363 363 'forum:Discussion' => link_to('Discussion', board_url, :class => 'board'),
364 364 # message
365 365 'message#4' => link_to('Post 2', message_url, :class => 'message'),
366 366 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5', :r => 5), :class => 'message'),
367 367 # news
368 368 'news#1' => link_to('eCookbook first release !', news_url, :class => 'news'),
369 369 'news:"eCookbook first release !"' => link_to('eCookbook first release !', news_url, :class => 'news'),
370 370 # project
371 371 'project#3' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
372 372 'project:subproject1' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
373 373 'project:"eCookbook subProject 1"' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
374 374 # not found
375 375 '#0123456789' => '#0123456789',
376 376 # invalid expressions
377 377 'source:' => 'source:',
378 378 # url hash
379 379 "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
380 380 }
381 381 @project = Project.find(1)
382 382 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
383 383 end
384 384
385 385 def test_should_not_parse_redmine_links_inside_link
386 386 raw = "r1 should not be parsed in http://example.com/url-r1/"
387 387 assert_match %r{<p><a class="changeset".*>r1</a> should not be parsed in <a class="external" href="http://example.com/url-r1/">http://example.com/url-r1/</a></p>},
388 388 textilizable(raw, :project => Project.find(1))
389 389 end
390 390
391 391 def test_redmine_links_with_a_different_project_before_current_project
392 392 vp1 = Version.generate!(:project_id => 1, :name => '1.4.4')
393 393 vp3 = Version.generate!(:project_id => 3, :name => '1.4.4')
394 394 @project = Project.find(3)
395 395 result1 = link_to("1.4.4", "/versions/#{vp1.id}", :class => "version")
396 396 result2 = link_to("1.4.4", "/versions/#{vp3.id}", :class => "version")
397 397 assert_equal "<p>#{result1} #{result2}</p>",
398 398 textilizable("ecookbook:version:1.4.4 version:1.4.4")
399 399 end
400 400
401 401 def test_escaped_redmine_links_should_not_be_parsed
402 402 to_test = [
403 403 '#3.',
404 404 '#3-14.',
405 405 '#3#-note14.',
406 406 'r1',
407 407 'document#1',
408 408 'document:"Test document"',
409 409 'version#2',
410 410 'version:1.0',
411 411 'version:"1.0"',
412 412 'source:/some/file'
413 413 ]
414 414 @project = Project.find(1)
415 415 to_test.each { |text| assert_equal "<p>#{text}</p>", textilizable("!" + text), "#{text} failed" }
416 416 end
417 417
418 418 def test_cross_project_redmine_links
419 419 source_link = link_to('ecookbook:source:/some/file',
420 420 {:controller => 'repositories', :action => 'entry',
421 421 :id => 'ecookbook', :path => ['some', 'file']},
422 422 :class => 'source')
423 423 changeset_link = link_to('ecookbook:r2',
424 424 {:controller => 'repositories', :action => 'revision',
425 425 :id => 'ecookbook', :rev => 2},
426 426 :class => 'changeset',
427 427 :title => 'This commit fixes #1, #2 and references #1 & #3')
428 428 to_test = {
429 429 # documents
430 430 'document:"Test document"' => 'document:"Test document"',
431 431 'ecookbook:document:"Test document"' =>
432 432 link_to("Test document", "/documents/1", :class => "document"),
433 433 'invalid:document:"Test document"' => 'invalid:document:"Test document"',
434 434 # versions
435 435 'version:"1.0"' => 'version:"1.0"',
436 436 'ecookbook:version:"1.0"' =>
437 437 link_to("1.0", "/versions/2", :class => "version"),
438 438 'invalid:version:"1.0"' => 'invalid:version:"1.0"',
439 439 # changeset
440 440 'r2' => 'r2',
441 441 'ecookbook:r2' => changeset_link,
442 442 'invalid:r2' => 'invalid:r2',
443 443 # source
444 444 'source:/some/file' => 'source:/some/file',
445 445 'ecookbook:source:/some/file' => source_link,
446 446 'invalid:source:/some/file' => 'invalid:source:/some/file',
447 447 }
448 448 @project = Project.find(3)
449 449 to_test.each do |text, result|
450 450 assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed"
451 451 end
452 452 end
453 453
454 454 def test_redmine_links_by_name_should_work_with_html_escaped_characters
455 455 v = Version.generate!(:name => "Test & Show.txt", :project_id => 1)
456 456 link = link_to("Test & Show.txt", "/versions/#{v.id}", :class => "version")
457 457
458 458 @project = v.project
459 459 assert_equal "<p>#{link}</p>", textilizable('version:"Test & Show.txt"')
460 460 end
461 461
462 462 def test_link_to_issue_subject
463 463 issue = Issue.generate!(:subject => "01234567890123456789")
464 464 str = link_to_issue(issue, :truncate => 10)
465 465 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}", :class => issue.css_classes)
466 466 assert_equal "#{result}: 0123456...", str
467 467
468 468 issue = Issue.generate!(:subject => "<&>")
469 469 str = link_to_issue(issue)
470 470 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}", :class => issue.css_classes)
471 471 assert_equal "#{result}: &lt;&amp;&gt;", str
472 472
473 473 issue = Issue.generate!(:subject => "<&>0123456789012345")
474 474 str = link_to_issue(issue, :truncate => 10)
475 475 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}", :class => issue.css_classes)
476 476 assert_equal "#{result}: &lt;&amp;&gt;0123...", str
477 477 end
478 478
479 479 def test_link_to_issue_title
480 480 long_str = "0123456789" * 5
481 481
482 482 issue = Issue.generate!(:subject => "#{long_str}01234567890123456789")
483 483 str = link_to_issue(issue, :subject => false)
484 484 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}",
485 485 :class => issue.css_classes,
486 486 :title => "#{long_str}0123456...")
487 487 assert_equal result, str
488 488
489 489 issue = Issue.generate!(:subject => "<&>#{long_str}01234567890123456789")
490 490 str = link_to_issue(issue, :subject => false)
491 491 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}",
492 492 :class => issue.css_classes,
493 493 :title => "<&>#{long_str}0123...")
494 494 assert_equal result, str
495 495 end
496 496
497 497 def test_multiple_repositories_redmine_links
498 498 svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn_repo-1', :url => 'file:///foo/hg')
499 499 Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
500 500 hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
501 501 Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
502 502
503 503 changeset_link = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
504 504 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
505 505 svn_changeset_link = link_to('svn_repo-1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn_repo-1', :rev => 123},
506 506 :class => 'changeset', :title => '')
507 507 hg_changeset_link = link_to('hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
508 508 :class => 'changeset', :title => '')
509 509
510 510 source_link = link_to('source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
511 511 hg_source_link = link_to('source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
512 512
513 513 to_test = {
514 514 'r2' => changeset_link,
515 515 'svn_repo-1|r123' => svn_changeset_link,
516 516 'invalid|r123' => 'invalid|r123',
517 517 'commit:hg1|abcd' => hg_changeset_link,
518 518 'commit:invalid|abcd' => 'commit:invalid|abcd',
519 519 # source
520 520 'source:some/file' => source_link,
521 521 'source:hg1|some/file' => hg_source_link,
522 522 'source:invalid|some/file' => 'source:invalid|some/file',
523 523 }
524 524
525 525 @project = Project.find(1)
526 526 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
527 527 end
528 528
529 529 def test_cross_project_multiple_repositories_redmine_links
530 530 svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn1', :url => 'file:///foo/hg')
531 531 Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
532 532 hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
533 533 Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
534 534
535 535 changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
536 536 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
537 537 svn_changeset_link = link_to('ecookbook:svn1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn1', :rev => 123},
538 538 :class => 'changeset', :title => '')
539 539 hg_changeset_link = link_to('ecookbook:hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
540 540 :class => 'changeset', :title => '')
541 541
542 542 source_link = link_to('ecookbook:source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
543 543 hg_source_link = link_to('ecookbook:source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
544 544
545 545 to_test = {
546 546 'ecookbook:r2' => changeset_link,
547 547 'ecookbook:svn1|r123' => svn_changeset_link,
548 548 'ecookbook:invalid|r123' => 'ecookbook:invalid|r123',
549 549 'ecookbook:commit:hg1|abcd' => hg_changeset_link,
550 550 'ecookbook:commit:invalid|abcd' => 'ecookbook:commit:invalid|abcd',
551 551 'invalid:commit:invalid|abcd' => 'invalid:commit:invalid|abcd',
552 552 # source
553 553 'ecookbook:source:some/file' => source_link,
554 554 'ecookbook:source:hg1|some/file' => hg_source_link,
555 555 'ecookbook:source:invalid|some/file' => 'ecookbook:source:invalid|some/file',
556 556 'invalid:source:invalid|some/file' => 'invalid:source:invalid|some/file',
557 557 }
558 558
559 559 @project = Project.find(3)
560 560 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
561 561 end
562 562
563 563 def test_redmine_links_git_commit
564 564 changeset_link = link_to('abcd',
565 565 {
566 566 :controller => 'repositories',
567 567 :action => 'revision',
568 568 :id => 'subproject1',
569 569 :rev => 'abcd',
570 570 },
571 571 :class => 'changeset', :title => 'test commit')
572 572 to_test = {
573 573 'commit:abcd' => changeset_link,
574 574 }
575 575 @project = Project.find(3)
576 576 r = Repository::Git.create!(:project => @project, :url => '/tmp/test/git')
577 577 assert r
578 578 c = Changeset.new(:repository => r,
579 579 :committed_on => Time.now,
580 580 :revision => 'abcd',
581 581 :scmid => 'abcd',
582 582 :comments => 'test commit')
583 583 assert( c.save )
584 584 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
585 585 end
586 586
587 587 # TODO: Bazaar commit id contains mail address, so it contains '@' and '_'.
588 588 def test_redmine_links_darcs_commit
589 589 changeset_link = link_to('20080308225258-98289-abcd456efg.gz',
590 590 {
591 591 :controller => 'repositories',
592 592 :action => 'revision',
593 593 :id => 'subproject1',
594 594 :rev => '123',
595 595 },
596 596 :class => 'changeset', :title => 'test commit')
597 597 to_test = {
598 598 'commit:20080308225258-98289-abcd456efg.gz' => changeset_link,
599 599 }
600 600 @project = Project.find(3)
601 601 r = Repository::Darcs.create!(
602 602 :project => @project, :url => '/tmp/test/darcs',
603 603 :log_encoding => 'UTF-8')
604 604 assert r
605 605 c = Changeset.new(:repository => r,
606 606 :committed_on => Time.now,
607 607 :revision => '123',
608 608 :scmid => '20080308225258-98289-abcd456efg.gz',
609 609 :comments => 'test commit')
610 610 assert( c.save )
611 611 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
612 612 end
613 613
614 614 def test_redmine_links_mercurial_commit
615 615 changeset_link_rev = link_to('r123',
616 616 {
617 617 :controller => 'repositories',
618 618 :action => 'revision',
619 619 :id => 'subproject1',
620 620 :rev => '123' ,
621 621 },
622 622 :class => 'changeset', :title => 'test commit')
623 623 changeset_link_commit = link_to('abcd',
624 624 {
625 625 :controller => 'repositories',
626 626 :action => 'revision',
627 627 :id => 'subproject1',
628 628 :rev => 'abcd' ,
629 629 },
630 630 :class => 'changeset', :title => 'test commit')
631 631 to_test = {
632 632 'r123' => changeset_link_rev,
633 633 'commit:abcd' => changeset_link_commit,
634 634 }
635 635 @project = Project.find(3)
636 636 r = Repository::Mercurial.create!(:project => @project, :url => '/tmp/test')
637 637 assert r
638 638 c = Changeset.new(:repository => r,
639 639 :committed_on => Time.now,
640 640 :revision => '123',
641 641 :scmid => 'abcd',
642 642 :comments => 'test commit')
643 643 assert( c.save )
644 644 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
645 645 end
646 646
647 647 def test_attachment_links
648 648 text = 'attachment:error281.txt'
649 649 result = link_to("error281.txt", "/attachments/download/1/error281.txt",
650 650 :class => "attachment")
651 651 assert_equal "<p>#{result}</p>",
652 652 textilizable(text,
653 653 :attachments => Issue.find(3).attachments),
654 654 "#{text} failed"
655 655 end
656 656
657 657 def test_attachment_link_should_link_to_latest_attachment
658 658 set_tmp_attachments_directory
659 659 a1 = Attachment.generate!(:filename => "test.txt", :created_on => 1.hour.ago)
660 660 a2 = Attachment.generate!(:filename => "test.txt")
661 661 result = link_to("test.txt", "/attachments/download/#{a2.id}/test.txt",
662 662 :class => "attachment")
663 663 assert_equal "<p>#{result}</p>",
664 664 textilizable('attachment:test.txt', :attachments => [a1, a2])
665 665 end
666 666
667 667 def test_wiki_links
668 668 russian_eacape = CGI.escape(@russian_test)
669 669 to_test = {
670 670 '[[CookBook documentation]]' =>
671 671 link_to("CookBook documentation",
672 672 "/projects/ecookbook/wiki/CookBook_documentation",
673 673 :class => "wiki-page"),
674 674 '[[Another page|Page]]' =>
675 675 link_to("Page",
676 676 "/projects/ecookbook/wiki/Another_page",
677 677 :class => "wiki-page"),
678 678 # title content should be formatted
679 679 '[[Another page|With _styled_ *title*]]' =>
680 680 link_to("With <em>styled</em> <strong>title</strong>".html_safe,
681 681 "/projects/ecookbook/wiki/Another_page",
682 682 :class => "wiki-page"),
683 683 '[[Another page|With title containing <strong>HTML entities &amp; markups</strong>]]' =>
684 684 link_to("With title containing &lt;strong&gt;HTML entities &amp; markups&lt;/strong&gt;".html_safe,
685 685 "/projects/ecookbook/wiki/Another_page",
686 686 :class => "wiki-page"),
687 687 # link with anchor
688 688 '[[CookBook documentation#One-section]]' =>
689 689 link_to("CookBook documentation",
690 690 "/projects/ecookbook/wiki/CookBook_documentation#One-section",
691 691 :class => "wiki-page"),
692 692 '[[Another page#anchor|Page]]' =>
693 693 link_to("Page",
694 694 "/projects/ecookbook/wiki/Another_page#anchor",
695 695 :class => "wiki-page"),
696 696 # UTF8 anchor
697 697 "[[Another_page##{@russian_test}|#{@russian_test}]]" =>
698 698 link_to(@russian_test,
699 699 "/projects/ecookbook/wiki/Another_page##{russian_eacape}",
700 700 :class => "wiki-page"),
701 701 # page that doesn't exist
702 702 '[[Unknown page]]' =>
703 703 link_to("Unknown page",
704 704 "/projects/ecookbook/wiki/Unknown_page",
705 705 :class => "wiki-page new"),
706 706 '[[Unknown page|404]]' =>
707 707 link_to("404",
708 708 "/projects/ecookbook/wiki/Unknown_page",
709 709 :class => "wiki-page new"),
710 710 # link to another project wiki
711 711 '[[onlinestore:]]' =>
712 712 link_to("onlinestore",
713 713 "/projects/onlinestore/wiki",
714 714 :class => "wiki-page"),
715 715 '[[onlinestore:|Wiki]]' =>
716 716 link_to("Wiki",
717 717 "/projects/onlinestore/wiki",
718 718 :class => "wiki-page"),
719 719 '[[onlinestore:Start page]]' =>
720 720 link_to("Start page",
721 721 "/projects/onlinestore/wiki/Start_page",
722 722 :class => "wiki-page"),
723 723 '[[onlinestore:Start page|Text]]' =>
724 724 link_to("Text",
725 725 "/projects/onlinestore/wiki/Start_page",
726 726 :class => "wiki-page"),
727 727 '[[onlinestore:Unknown page]]' =>
728 728 link_to("Unknown page",
729 729 "/projects/onlinestore/wiki/Unknown_page",
730 730 :class => "wiki-page new"),
731 731 # struck through link
732 732 '-[[Another page|Page]]-' =>
733 733 "<del>".html_safe +
734 734 link_to("Page",
735 735 "/projects/ecookbook/wiki/Another_page",
736 736 :class => "wiki-page").html_safe +
737 737 "</del>".html_safe,
738 738 '-[[Another page|Page]] link-' =>
739 739 "<del>".html_safe +
740 740 link_to("Page",
741 741 "/projects/ecookbook/wiki/Another_page",
742 742 :class => "wiki-page").html_safe +
743 743 " link</del>".html_safe,
744 744 # escaping
745 745 '![[Another page|Page]]' => '[[Another page|Page]]',
746 746 # project does not exist
747 747 '[[unknowproject:Start]]' => '[[unknowproject:Start]]',
748 748 '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]',
749 749 }
750 750 @project = Project.find(1)
751 751 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
752 752 end
753 753
754 754 def test_wiki_links_within_local_file_generation_context
755 755 to_test = {
756 756 # link to a page
757 757 '[[CookBook documentation]]' =>
758 758 link_to("CookBook documentation", "CookBook_documentation.html",
759 759 :class => "wiki-page"),
760 760 '[[CookBook documentation|documentation]]' =>
761 761 link_to("documentation", "CookBook_documentation.html",
762 762 :class => "wiki-page"),
763 763 '[[CookBook documentation#One-section]]' =>
764 764 link_to("CookBook documentation", "CookBook_documentation.html#One-section",
765 765 :class => "wiki-page"),
766 766 '[[CookBook documentation#One-section|documentation]]' =>
767 767 link_to("documentation", "CookBook_documentation.html#One-section",
768 768 :class => "wiki-page"),
769 769 # page that doesn't exist
770 770 '[[Unknown page]]' =>
771 771 link_to("Unknown page", "Unknown_page.html",
772 772 :class => "wiki-page new"),
773 773 '[[Unknown page|404]]' =>
774 774 link_to("404", "Unknown_page.html",
775 775 :class => "wiki-page new"),
776 776 '[[Unknown page#anchor]]' =>
777 777 link_to("Unknown page", "Unknown_page.html#anchor",
778 778 :class => "wiki-page new"),
779 779 '[[Unknown page#anchor|404]]' =>
780 780 link_to("404", "Unknown_page.html#anchor",
781 781 :class => "wiki-page new"),
782 782 }
783 783 @project = Project.find(1)
784 784 to_test.each do |text, result|
785 785 assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :local)
786 786 end
787 787 end
788 788
789 789 def test_wiki_links_within_wiki_page_context
790 790 page = WikiPage.find_by_title('Another_page' )
791 791 to_test = {
792 792 '[[CookBook documentation]]' =>
793 793 link_to("CookBook documentation",
794 794 "/projects/ecookbook/wiki/CookBook_documentation",
795 795 :class => "wiki-page"),
796 796 '[[CookBook documentation|documentation]]' =>
797 797 link_to("documentation",
798 798 "/projects/ecookbook/wiki/CookBook_documentation",
799 799 :class => "wiki-page"),
800 800 '[[CookBook documentation#One-section]]' =>
801 801 link_to("CookBook documentation",
802 802 "/projects/ecookbook/wiki/CookBook_documentation#One-section",
803 803 :class => "wiki-page"),
804 804 '[[CookBook documentation#One-section|documentation]]' =>
805 805 link_to("documentation",
806 806 "/projects/ecookbook/wiki/CookBook_documentation#One-section",
807 807 :class => "wiki-page"),
808 808 # link to the current page
809 809 '[[Another page]]' =>
810 810 link_to("Another page",
811 811 "/projects/ecookbook/wiki/Another_page",
812 812 :class => "wiki-page"),
813 813 '[[Another page|Page]]' =>
814 814 link_to("Page",
815 815 "/projects/ecookbook/wiki/Another_page",
816 816 :class => "wiki-page"),
817 817 '[[Another page#anchor]]' =>
818 818 link_to("Another page",
819 819 "#anchor",
820 820 :class => "wiki-page"),
821 821 '[[Another page#anchor|Page]]' =>
822 822 link_to("Page",
823 823 "#anchor",
824 824 :class => "wiki-page"),
825 825 # page that doesn't exist
826 826 '[[Unknown page]]' =>
827 827 link_to("Unknown page",
828 828 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page",
829 829 :class => "wiki-page new"),
830 830 '[[Unknown page|404]]' =>
831 831 link_to("404",
832 832 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page",
833 833 :class => "wiki-page new"),
834 834 '[[Unknown page#anchor]]' =>
835 835 link_to("Unknown page",
836 836 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor",
837 837 :class => "wiki-page new"),
838 838 '[[Unknown page#anchor|404]]' =>
839 839 link_to("404",
840 840 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor",
841 841 :class => "wiki-page new"),
842 842 }
843 843 @project = Project.find(1)
844 844 to_test.each do |text, result|
845 845 assert_equal "<p>#{result}</p>",
846 846 textilizable(WikiContent.new( :text => text, :page => page ), :text)
847 847 end
848 848 end
849 849
850 850 def test_wiki_links_anchor_option_should_prepend_page_title_to_href
851 851 to_test = {
852 852 # link to a page
853 853 '[[CookBook documentation]]' =>
854 854 link_to("CookBook documentation",
855 855 "#CookBook_documentation",
856 856 :class => "wiki-page"),
857 857 '[[CookBook documentation|documentation]]' =>
858 858 link_to("documentation",
859 859 "#CookBook_documentation",
860 860 :class => "wiki-page"),
861 861 '[[CookBook documentation#One-section]]' =>
862 862 link_to("CookBook documentation",
863 863 "#CookBook_documentation_One-section",
864 864 :class => "wiki-page"),
865 865 '[[CookBook documentation#One-section|documentation]]' =>
866 866 link_to("documentation",
867 867 "#CookBook_documentation_One-section",
868 868 :class => "wiki-page"),
869 869 # page that doesn't exist
870 870 '[[Unknown page]]' =>
871 871 link_to("Unknown page",
872 872 "#Unknown_page",
873 873 :class => "wiki-page new"),
874 874 '[[Unknown page|404]]' =>
875 875 link_to("404",
876 876 "#Unknown_page",
877 877 :class => "wiki-page new"),
878 878 '[[Unknown page#anchor]]' =>
879 879 link_to("Unknown page",
880 880 "#Unknown_page_anchor",
881 881 :class => "wiki-page new"),
882 882 '[[Unknown page#anchor|404]]' =>
883 883 link_to("404",
884 884 "#Unknown_page_anchor",
885 885 :class => "wiki-page new"),
886 886 }
887 887 @project = Project.find(1)
888 888 to_test.each do |text, result|
889 889 assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :anchor)
890 890 end
891 891 end
892 892
893 893 def test_html_tags
894 894 to_test = {
895 895 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
896 896 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
897 897 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
898 898 # do not escape pre/code tags
899 899 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
900 900 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
901 901 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
902 902 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
903 903 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
904 904 # remove attributes except class
905 905 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
906 906 '<pre class="foo">some text</pre>' => '<pre class="foo">some text</pre>',
907 907 "<pre class='foo bar'>some text</pre>" => "<pre class='foo bar'>some text</pre>",
908 908 '<pre class="foo bar">some text</pre>' => '<pre class="foo bar">some text</pre>',
909 909 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
910 910 # xss
911 911 '<pre><code class=""onmouseover="alert(1)">text</code></pre>' => '<pre><code>text</code></pre>',
912 912 '<pre class=""onmouseover="alert(1)">text</pre>' => '<pre>text</pre>',
913 913 }
914 914 to_test.each { |text, result| assert_equal result, textilizable(text) }
915 915 end
916 916
917 917 def test_allowed_html_tags
918 918 to_test = {
919 919 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
920 920 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
921 921 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
922 922 }
923 923 to_test.each { |text, result| assert_equal result, textilizable(text) }
924 924 end
925 925
926 926 def test_pre_tags
927 927 raw = <<-RAW
928 928 Before
929 929
930 930 <pre>
931 931 <prepared-statement-cache-size>32</prepared-statement-cache-size>
932 932 </pre>
933 933
934 934 After
935 935 RAW
936 936
937 937 expected = <<-EXPECTED
938 938 <p>Before</p>
939 939 <pre>
940 940 &lt;prepared-statement-cache-size&gt;32&lt;/prepared-statement-cache-size&gt;
941 941 </pre>
942 942 <p>After</p>
943 943 EXPECTED
944 944
945 945 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
946 946 end
947 947
948 948 def test_pre_content_should_not_parse_wiki_and_redmine_links
949 949 raw = <<-RAW
950 950 [[CookBook documentation]]
951 951
952 952 #1
953 953
954 954 <pre>
955 955 [[CookBook documentation]]
956 956
957 957 #1
958 958 </pre>
959 959 RAW
960 960
961 961 result1 = link_to("CookBook documentation",
962 962 "/projects/ecookbook/wiki/CookBook_documentation",
963 963 :class => "wiki-page")
964 964 result2 = link_to('#1',
965 965 "/issues/1",
966 966 :class => Issue.find(1).css_classes,
967 :title => "Cannot print recipes (New)")
967 :title => "Bug: Cannot print recipes (New)")
968 968
969 969 expected = <<-EXPECTED
970 970 <p>#{result1}</p>
971 971 <p>#{result2}</p>
972 972 <pre>
973 973 [[CookBook documentation]]
974 974
975 975 #1
976 976 </pre>
977 977 EXPECTED
978 978
979 979 @project = Project.find(1)
980 980 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
981 981 end
982 982
983 983 def test_non_closing_pre_blocks_should_be_closed
984 984 raw = <<-RAW
985 985 <pre><code>
986 986 RAW
987 987
988 988 expected = <<-EXPECTED
989 989 <pre><code>
990 990 </code></pre>
991 991 EXPECTED
992 992
993 993 @project = Project.find(1)
994 994 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
995 995 end
996 996
997 997 def test_syntax_highlight
998 998 raw = <<-RAW
999 999 <pre><code class="ruby">
1000 1000 # Some ruby code here
1001 1001 </code></pre>
1002 1002 RAW
1003 1003
1004 1004 expected = <<-EXPECTED
1005 1005 <pre><code class="ruby syntaxhl"><span class=\"CodeRay\"><span class="comment"># Some ruby code here</span></span>
1006 1006 </code></pre>
1007 1007 EXPECTED
1008 1008
1009 1009 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
1010 1010 end
1011 1011
1012 1012 def test_to_path_param
1013 1013 assert_equal 'test1/test2', to_path_param('test1/test2')
1014 1014 assert_equal 'test1/test2', to_path_param('/test1/test2/')
1015 1015 assert_equal 'test1/test2', to_path_param('//test1/test2/')
1016 1016 assert_equal nil, to_path_param('/')
1017 1017 end
1018 1018
1019 1019 def test_wiki_links_in_tables
1020 1020 text = "|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|"
1021 1021 link1 = link_to("Link title", "/projects/ecookbook/wiki/Page", :class => "wiki-page new")
1022 1022 link2 = link_to("Other title", "/projects/ecookbook/wiki/Other_Page", :class => "wiki-page new")
1023 1023 link3 = link_to("Last page", "/projects/ecookbook/wiki/Last_page", :class => "wiki-page new")
1024 1024 result = "<tr><td>#{link1}</td>" +
1025 1025 "<td>#{link2}</td>" +
1026 1026 "</tr><tr><td>Cell 21</td><td>#{link3}</td></tr>"
1027 1027 @project = Project.find(1)
1028 1028 assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '')
1029 1029 end
1030 1030
1031 1031 def test_text_formatting
1032 1032 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
1033 1033 '(_text within parentheses_)' => '(<em>text within parentheses</em>)',
1034 1034 'a *Humane Web* Text Generator' => 'a <strong>Humane Web</strong> Text Generator',
1035 1035 'a H *umane* W *eb* T *ext* G *enerator*' => 'a H <strong>umane</strong> W <strong>eb</strong> T <strong>ext</strong> G <strong>enerator</strong>',
1036 1036 'a *H* umane *W* eb *T* ext *G* enerator' => 'a <strong>H</strong> umane <strong>W</strong> eb <strong>T</strong> ext <strong>G</strong> enerator',
1037 1037 }
1038 1038 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
1039 1039 end
1040 1040
1041 1041 def test_wiki_horizontal_rule
1042 1042 assert_equal '<hr />', textilizable('---')
1043 1043 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
1044 1044 end
1045 1045
1046 1046 def test_footnotes
1047 1047 raw = <<-RAW
1048 1048 This is some text[1].
1049 1049
1050 1050 fn1. This is the foot note
1051 1051 RAW
1052 1052
1053 1053 expected = <<-EXPECTED
1054 1054 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
1055 1055 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
1056 1056 EXPECTED
1057 1057
1058 1058 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
1059 1059 end
1060 1060
1061 1061 def test_headings
1062 1062 raw = 'h1. Some heading'
1063 1063 expected = %|<a name="Some-heading"></a>\n<h1 >Some heading<a href="#Some-heading" class="wiki-anchor">&para;</a></h1>|
1064 1064
1065 1065 assert_equal expected, textilizable(raw)
1066 1066 end
1067 1067
1068 1068 def test_headings_with_special_chars
1069 1069 # This test makes sure that the generated anchor names match the expected
1070 1070 # ones even if the heading text contains unconventional characters
1071 1071 raw = 'h1. Some heading related to version 0.5'
1072 1072 anchor = sanitize_anchor_name("Some-heading-related-to-version-0.5")
1073 1073 expected = %|<a name="#{anchor}"></a>\n<h1 >Some heading related to version 0.5<a href="##{anchor}" class="wiki-anchor">&para;</a></h1>|
1074 1074
1075 1075 assert_equal expected, textilizable(raw)
1076 1076 end
1077 1077
1078 1078 def test_headings_in_wiki_single_page_export_should_be_prepended_with_page_title
1079 1079 page = WikiPage.new( :title => 'Page Title', :wiki_id => 1 )
1080 1080 content = WikiContent.new( :text => 'h1. Some heading', :page => page )
1081 1081
1082 1082 expected = %|<a name="Page_Title_Some-heading"></a>\n<h1 >Some heading<a href="#Page_Title_Some-heading" class="wiki-anchor">&para;</a></h1>|
1083 1083
1084 1084 assert_equal expected, textilizable(content, :text, :wiki_links => :anchor )
1085 1085 end
1086 1086
1087 1087 def test_table_of_content
1088 1088 raw = <<-RAW
1089 1089 {{toc}}
1090 1090
1091 1091 h1. Title
1092 1092
1093 1093 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
1094 1094
1095 1095 h2. Subtitle with a [[Wiki]] link
1096 1096
1097 1097 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
1098 1098
1099 1099 h2. Subtitle with [[Wiki|another Wiki]] link
1100 1100
1101 1101 h2. Subtitle with %{color:red}red text%
1102 1102
1103 1103 <pre>
1104 1104 some code
1105 1105 </pre>
1106 1106
1107 1107 h3. Subtitle with *some* _modifiers_
1108 1108
1109 1109 h3. Subtitle with @inline code@
1110 1110
1111 1111 h1. Another title
1112 1112
1113 1113 h3. An "Internet link":http://www.redmine.org/ inside subtitle
1114 1114
1115 1115 h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
1116 1116
1117 1117 RAW
1118 1118
1119 1119 expected = '<ul class="toc">' +
1120 1120 '<li><a href="#Title">Title</a>' +
1121 1121 '<ul>' +
1122 1122 '<li><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
1123 1123 '<li><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
1124 1124 '<li><a href="#Subtitle-with-red-text">Subtitle with red text</a>' +
1125 1125 '<ul>' +
1126 1126 '<li><a href="#Subtitle-with-some-modifiers">Subtitle with some modifiers</a></li>' +
1127 1127 '<li><a href="#Subtitle-with-inline-code">Subtitle with inline code</a></li>' +
1128 1128 '</ul>' +
1129 1129 '</li>' +
1130 1130 '</ul>' +
1131 1131 '</li>' +
1132 1132 '<li><a href="#Another-title">Another title</a>' +
1133 1133 '<ul>' +
1134 1134 '<li>' +
1135 1135 '<ul>' +
1136 1136 '<li><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
1137 1137 '</ul>' +
1138 1138 '</li>' +
1139 1139 '<li><a href="#Project-Name">Project Name</a></li>' +
1140 1140 '</ul>' +
1141 1141 '</li>' +
1142 1142 '</ul>'
1143 1143
1144 1144 @project = Project.find(1)
1145 1145 assert textilizable(raw).gsub("\n", "").include?(expected)
1146 1146 end
1147 1147
1148 1148 def test_table_of_content_should_generate_unique_anchors
1149 1149 raw = <<-RAW
1150 1150 {{toc}}
1151 1151
1152 1152 h1. Title
1153 1153
1154 1154 h2. Subtitle
1155 1155
1156 1156 h2. Subtitle
1157 1157 RAW
1158 1158
1159 1159 expected = '<ul class="toc">' +
1160 1160 '<li><a href="#Title">Title</a>' +
1161 1161 '<ul>' +
1162 1162 '<li><a href="#Subtitle">Subtitle</a></li>' +
1163 1163 '<li><a href="#Subtitle-2">Subtitle</a></li>'
1164 1164 '</ul>'
1165 1165 '</li>' +
1166 1166 '</ul>'
1167 1167
1168 1168 @project = Project.find(1)
1169 1169 result = textilizable(raw).gsub("\n", "")
1170 1170 assert_include expected, result
1171 1171 assert_include '<a name="Subtitle">', result
1172 1172 assert_include '<a name="Subtitle-2">', result
1173 1173 end
1174 1174
1175 1175 def test_table_of_content_should_contain_included_page_headings
1176 1176 raw = <<-RAW
1177 1177 {{toc}}
1178 1178
1179 1179 h1. Included
1180 1180
1181 1181 {{include(Child_1)}}
1182 1182 RAW
1183 1183
1184 1184 expected = '<ul class="toc">' +
1185 1185 '<li><a href="#Included">Included</a></li>' +
1186 1186 '<li><a href="#Child-page-1">Child page 1</a></li>' +
1187 1187 '</ul>'
1188 1188
1189 1189 @project = Project.find(1)
1190 1190 assert textilizable(raw).gsub("\n", "").include?(expected)
1191 1191 end
1192 1192
1193 1193 def test_toc_with_textile_formatting_should_be_parsed
1194 1194 with_settings :text_formatting => 'textile' do
1195 1195 assert_select_in textilizable("{{toc}}\n\nh1. Heading"), 'ul.toc li', :text => 'Heading'
1196 1196 assert_select_in textilizable("{{<toc}}\n\nh1. Heading"), 'ul.toc.left li', :text => 'Heading'
1197 1197 assert_select_in textilizable("{{>toc}}\n\nh1. Heading"), 'ul.toc.right li', :text => 'Heading'
1198 1198 end
1199 1199 end
1200 1200
1201 1201 if Object.const_defined?(:Redcarpet)
1202 1202 def test_toc_with_markdown_formatting_should_be_parsed
1203 1203 with_settings :text_formatting => 'markdown' do
1204 1204 assert_select_in textilizable("{{toc}}\n\n# Heading"), 'ul.toc li', :text => 'Heading'
1205 1205 assert_select_in textilizable("{{<toc}}\n\n# Heading"), 'ul.toc.left li', :text => 'Heading'
1206 1206 assert_select_in textilizable("{{>toc}}\n\n# Heading"), 'ul.toc.right li', :text => 'Heading'
1207 1207 end
1208 1208 end
1209 1209 end
1210 1210
1211 1211 def test_section_edit_links
1212 1212 raw = <<-RAW
1213 1213 h1. Title
1214 1214
1215 1215 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
1216 1216
1217 1217 h2. Subtitle with a [[Wiki]] link
1218 1218
1219 1219 h2. Subtitle with *some* _modifiers_
1220 1220
1221 1221 h2. Subtitle with @inline code@
1222 1222
1223 1223 <pre>
1224 1224 some code
1225 1225
1226 1226 h2. heading inside pre
1227 1227
1228 1228 <h2>html heading inside pre</h2>
1229 1229 </pre>
1230 1230
1231 1231 h2. Subtitle after pre tag
1232 1232 RAW
1233 1233
1234 1234 @project = Project.find(1)
1235 1235 set_language_if_valid 'en'
1236 1236 result = textilizable(raw, :edit_section_links => {:controller => 'wiki', :action => 'edit', :project_id => '1', :id => 'Test'}).gsub("\n", "")
1237 1237
1238 1238 # heading that contains inline code
1239 1239 assert_match Regexp.new('<div class="contextual" title="Edit this section" id="section-4">' +
1240 1240 '<a href="/projects/1/wiki/Test/edit\?section=4"><img src="/images/edit.png(\?\d+)?" alt="Edit" /></a></div>' +
1241 1241 '<a name="Subtitle-with-inline-code"></a>' +
1242 1242 '<h2 >Subtitle with <code>inline code</code><a href="#Subtitle-with-inline-code" class="wiki-anchor">&para;</a></h2>'),
1243 1243 result
1244 1244
1245 1245 # last heading
1246 1246 assert_match Regexp.new('<div class="contextual" title="Edit this section" id="section-5">' +
1247 1247 '<a href="/projects/1/wiki/Test/edit\?section=5"><img src="/images/edit.png(\?\d+)?" alt="Edit" /></a></div>' +
1248 1248 '<a name="Subtitle-after-pre-tag"></a>' +
1249 1249 '<h2 >Subtitle after pre tag<a href="#Subtitle-after-pre-tag" class="wiki-anchor">&para;</a></h2>'),
1250 1250 result
1251 1251 end
1252 1252
1253 1253 def test_default_formatter
1254 1254 with_settings :text_formatting => 'unknown' do
1255 1255 text = 'a *link*: http://www.example.net/'
1256 1256 assert_equal '<p>a *link*: <a class="external" href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
1257 1257 end
1258 1258 end
1259 1259
1260 1260 def test_due_date_distance_in_words
1261 1261 to_test = { Date.today => 'Due in 0 days',
1262 1262 Date.today + 1 => 'Due in 1 day',
1263 1263 Date.today + 100 => 'Due in about 3 months',
1264 1264 Date.today + 20000 => 'Due in over 54 years',
1265 1265 Date.today - 1 => '1 day late',
1266 1266 Date.today - 100 => 'about 3 months late',
1267 1267 Date.today - 20000 => 'over 54 years late',
1268 1268 }
1269 1269 ::I18n.locale = :en
1270 1270 to_test.each do |date, expected|
1271 1271 assert_equal expected, due_date_distance_in_words(date)
1272 1272 end
1273 1273 end
1274 1274
1275 1275 def test_avatar_enabled
1276 1276 with_settings :gravatar_enabled => '1' do
1277 1277 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
1278 1278 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
1279 1279 # Default size is 50
1280 1280 assert avatar('jsmith <jsmith@somenet.foo>').include?('size=50')
1281 1281 assert avatar('jsmith <jsmith@somenet.foo>', :size => 24).include?('size=24')
1282 1282 # Non-avatar options should be considered html options
1283 1283 assert avatar('jsmith <jsmith@somenet.foo>', :title => 'John Smith').include?('title="John Smith"')
1284 1284 # The default class of the img tag should be gravatar
1285 1285 assert avatar('jsmith <jsmith@somenet.foo>').include?('class="gravatar"')
1286 1286 assert !avatar('jsmith <jsmith@somenet.foo>', :class => 'picture').include?('class="gravatar"')
1287 1287 assert_nil avatar('jsmith')
1288 1288 assert_nil avatar(nil)
1289 1289 end
1290 1290 end
1291 1291
1292 1292 def test_avatar_disabled
1293 1293 with_settings :gravatar_enabled => '0' do
1294 1294 assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
1295 1295 end
1296 1296 end
1297 1297
1298 1298 def test_link_to_user
1299 1299 user = User.find(2)
1300 1300 result = link_to("John Smith", "/users/2", :class => "user active")
1301 1301 assert_equal result, link_to_user(user)
1302 1302 end
1303 1303
1304 1304 def test_link_to_user_should_not_link_to_locked_user
1305 1305 with_current_user nil do
1306 1306 user = User.find(5)
1307 1307 assert user.locked?
1308 1308 assert_equal 'Dave2 Lopper2', link_to_user(user)
1309 1309 end
1310 1310 end
1311 1311
1312 1312 def test_link_to_user_should_link_to_locked_user_if_current_user_is_admin
1313 1313 with_current_user User.find(1) do
1314 1314 user = User.find(5)
1315 1315 assert user.locked?
1316 1316 result = link_to("Dave2 Lopper2", "/users/5", :class => "user locked")
1317 1317 assert_equal result, link_to_user(user)
1318 1318 end
1319 1319 end
1320 1320
1321 1321 def test_link_to_user_should_not_link_to_anonymous
1322 1322 user = User.anonymous
1323 1323 assert user.anonymous?
1324 1324 t = link_to_user(user)
1325 1325 assert_equal ::I18n.t(:label_user_anonymous), t
1326 1326 end
1327 1327
1328 1328 def test_link_to_attachment
1329 1329 a = Attachment.find(3)
1330 1330 assert_equal '<a href="/attachments/3/logo.gif">logo.gif</a>',
1331 1331 link_to_attachment(a)
1332 1332 assert_equal '<a href="/attachments/3/logo.gif">Text</a>',
1333 1333 link_to_attachment(a, :text => 'Text')
1334 1334 result = link_to("logo.gif", "/attachments/3/logo.gif", :class => "foo")
1335 1335 assert_equal result,
1336 1336 link_to_attachment(a, :class => 'foo')
1337 1337 assert_equal '<a href="/attachments/download/3/logo.gif">logo.gif</a>',
1338 1338 link_to_attachment(a, :download => true)
1339 1339 assert_equal '<a href="http://test.host/attachments/3/logo.gif">logo.gif</a>',
1340 1340 link_to_attachment(a, :only_path => false)
1341 1341 end
1342 1342
1343 1343 def test_thumbnail_tag
1344 1344 a = Attachment.find(3)
1345 1345 assert_select_in thumbnail_tag(a),
1346 1346 'a[href=?][title=?] img[alt="3"][src=?]',
1347 1347 "/attachments/3/logo.gif", "logo.gif", "/attachments/thumbnail/3"
1348 1348 end
1349 1349
1350 1350 def test_link_to_project
1351 1351 project = Project.find(1)
1352 1352 assert_equal %(<a href="/projects/ecookbook">eCookbook</a>),
1353 1353 link_to_project(project)
1354 1354 assert_equal %(<a href="http://test.host/projects/ecookbook?jump=blah">eCookbook</a>),
1355 1355 link_to_project(project, {:only_path => false, :jump => 'blah'})
1356 1356 end
1357 1357
1358 1358 def test_link_to_project_settings
1359 1359 project = Project.find(1)
1360 1360 assert_equal '<a href="/projects/ecookbook/settings">eCookbook</a>', link_to_project_settings(project)
1361 1361
1362 1362 project.status = Project::STATUS_CLOSED
1363 1363 assert_equal '<a href="/projects/ecookbook">eCookbook</a>', link_to_project_settings(project)
1364 1364
1365 1365 project.status = Project::STATUS_ARCHIVED
1366 1366 assert_equal 'eCookbook', link_to_project_settings(project)
1367 1367 end
1368 1368
1369 1369 def test_link_to_legacy_project_with_numerical_identifier_should_use_id
1370 1370 # numeric identifier are no longer allowed
1371 1371 Project.where(:id => 1).update_all(:identifier => 25)
1372 1372 assert_equal '<a href="/projects/1">eCookbook</a>',
1373 1373 link_to_project(Project.find(1))
1374 1374 end
1375 1375
1376 1376 def test_principals_options_for_select_with_users
1377 1377 User.current = nil
1378 1378 users = [User.find(2), User.find(4)]
1379 1379 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>),
1380 1380 principals_options_for_select(users)
1381 1381 end
1382 1382
1383 1383 def test_principals_options_for_select_with_selected
1384 1384 User.current = nil
1385 1385 users = [User.find(2), User.find(4)]
1386 1386 assert_equal %(<option value="2">John Smith</option><option value="4" selected="selected">Robert Hill</option>),
1387 1387 principals_options_for_select(users, User.find(4))
1388 1388 end
1389 1389
1390 1390 def test_principals_options_for_select_with_users_and_groups
1391 1391 User.current = nil
1392 1392 set_language_if_valid 'en'
1393 1393 users = [User.find(2), Group.find(11), User.find(4), Group.find(10)]
1394 1394 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>) +
1395 1395 %(<optgroup label="Groups"><option value="10">A Team</option><option value="11">B Team</option></optgroup>),
1396 1396 principals_options_for_select(users)
1397 1397 end
1398 1398
1399 1399 def test_principals_options_for_select_with_empty_collection
1400 1400 assert_equal '', principals_options_for_select([])
1401 1401 end
1402 1402
1403 1403 def test_principals_options_for_select_should_include_me_option_when_current_user_is_in_collection
1404 1404 set_language_if_valid 'en'
1405 1405 users = [User.find(2), User.find(4)]
1406 1406 User.current = User.find(4)
1407 1407 assert_include '<option value="4">&lt;&lt; me &gt;&gt;</option>', principals_options_for_select(users)
1408 1408 end
1409 1409
1410 1410 def test_stylesheet_link_tag_should_pick_the_default_stylesheet
1411 1411 assert_match 'href="/stylesheets/styles.css"', stylesheet_link_tag("styles")
1412 1412 end
1413 1413
1414 1414 def test_stylesheet_link_tag_for_plugin_should_pick_the_plugin_stylesheet
1415 1415 assert_match 'href="/plugin_assets/foo/stylesheets/styles.css"', stylesheet_link_tag("styles", :plugin => :foo)
1416 1416 end
1417 1417
1418 1418 def test_image_tag_should_pick_the_default_image
1419 1419 assert_match 'src="/images/image.png"', image_tag("image.png")
1420 1420 end
1421 1421
1422 1422 def test_image_tag_should_pick_the_theme_image_if_it_exists
1423 1423 theme = Redmine::Themes.themes.last
1424 1424 theme.images << 'image.png'
1425 1425
1426 1426 with_settings :ui_theme => theme.id do
1427 1427 assert_match %|src="/themes/#{theme.dir}/images/image.png"|, image_tag("image.png")
1428 1428 assert_match %|src="/images/other.png"|, image_tag("other.png")
1429 1429 end
1430 1430 ensure
1431 1431 theme.images.delete 'image.png'
1432 1432 end
1433 1433
1434 1434 def test_image_tag_sfor_plugin_should_pick_the_plugin_image
1435 1435 assert_match 'src="/plugin_assets/foo/images/image.png"', image_tag("image.png", :plugin => :foo)
1436 1436 end
1437 1437
1438 1438 def test_javascript_include_tag_should_pick_the_default_javascript
1439 1439 assert_match 'src="/javascripts/scripts.js"', javascript_include_tag("scripts")
1440 1440 end
1441 1441
1442 1442 def test_javascript_include_tag_for_plugin_should_pick_the_plugin_javascript
1443 1443 assert_match 'src="/plugin_assets/foo/javascripts/scripts.js"', javascript_include_tag("scripts", :plugin => :foo)
1444 1444 end
1445 1445
1446 1446 def test_raw_json_should_escape_closing_tags
1447 1447 s = raw_json(["<foo>bar</foo>"])
1448 1448 assert_include '\/foo', s
1449 1449 end
1450 1450
1451 1451 def test_raw_json_should_be_html_safe
1452 1452 s = raw_json(["foo"])
1453 1453 assert s.html_safe?
1454 1454 end
1455 1455
1456 1456 def test_html_title_should_app_title_if_not_set
1457 1457 assert_equal 'Redmine', html_title
1458 1458 end
1459 1459
1460 1460 def test_html_title_should_join_items
1461 1461 html_title 'Foo', 'Bar'
1462 1462 assert_equal 'Foo - Bar - Redmine', html_title
1463 1463 end
1464 1464
1465 1465 def test_html_title_should_append_current_project_name
1466 1466 @project = Project.find(1)
1467 1467 html_title 'Foo', 'Bar'
1468 1468 assert_equal 'Foo - Bar - eCookbook - Redmine', html_title
1469 1469 end
1470 1470
1471 1471 def test_title_should_return_a_h2_tag
1472 1472 assert_equal '<h2>Foo</h2>', title('Foo')
1473 1473 end
1474 1474
1475 1475 def test_title_should_set_html_title
1476 1476 title('Foo')
1477 1477 assert_equal 'Foo - Redmine', html_title
1478 1478 end
1479 1479
1480 1480 def test_title_should_turn_arrays_into_links
1481 1481 assert_equal '<h2><a href="/foo">Foo</a></h2>', title(['Foo', '/foo'])
1482 1482 assert_equal 'Foo - Redmine', html_title
1483 1483 end
1484 1484
1485 1485 def test_title_should_join_items
1486 1486 assert_equal '<h2>Foo &#187; Bar</h2>', title('Foo', 'Bar')
1487 1487 assert_equal 'Bar - Foo - Redmine', html_title
1488 1488 end
1489 1489
1490 1490 def test_favicon_path
1491 1491 assert_match %r{^/favicon\.ico}, favicon_path
1492 1492 end
1493 1493
1494 1494 def test_favicon_path_with_suburi
1495 1495 Redmine::Utils.relative_url_root = '/foo'
1496 1496 assert_match %r{^/foo/favicon\.ico}, favicon_path
1497 1497 ensure
1498 1498 Redmine::Utils.relative_url_root = ''
1499 1499 end
1500 1500
1501 1501 def test_favicon_url
1502 1502 assert_match %r{^http://test\.host/favicon\.ico}, favicon_url
1503 1503 end
1504 1504
1505 1505 def test_favicon_url_with_suburi
1506 1506 Redmine::Utils.relative_url_root = '/foo'
1507 1507 assert_match %r{^http://test\.host/foo/favicon\.ico}, favicon_url
1508 1508 ensure
1509 1509 Redmine::Utils.relative_url_root = ''
1510 1510 end
1511 1511
1512 1512 def test_truncate_single_line
1513 1513 str = "01234"
1514 1514 result = truncate_single_line_raw("#{str}\n#{str}", 10)
1515 1515 assert_equal "01234 0...", result
1516 1516 assert !result.html_safe?
1517 1517 result = truncate_single_line_raw("#{str}<&#>\n#{str}#{str}", 16)
1518 1518 assert_equal "01234<&#> 012...", result
1519 1519 assert !result.html_safe?
1520 1520 end
1521 1521
1522 1522 def test_truncate_single_line_non_ascii
1523 1523 ja = "\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e".force_encoding('UTF-8')
1524 1524 result = truncate_single_line_raw("#{ja}\n#{ja}\n#{ja}", 10)
1525 1525 assert_equal "#{ja} #{ja}...", result
1526 1526 assert !result.html_safe?
1527 1527 end
1528 1528 end
@@ -1,853 +1,853
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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 Rails::Dom::Testing::Assertions
23 23 fixtures :projects, :enabled_modules, :issues, :users, :email_addresses, :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 Setting.default_language = 'en'
38 38 User.current = nil
39 39 end
40 40
41 41 def test_generated_links_in_emails
42 42 Setting.host_name = 'mydomain.foo'
43 43 Setting.protocol = 'https'
44 44
45 45 journal = Journal.find(3)
46 46 assert Mailer.deliver_issue_edit(journal)
47 47
48 48 mail = last_email
49 49 assert_not_nil mail
50 50
51 51 assert_select_email do
52 52 # link to the main ticket
53 53 assert_select 'a[href=?]',
54 54 'https://mydomain.foo/issues/2#change-3',
55 55 :text => 'Feature request #2: Add ingredients categories'
56 56 # link to a referenced ticket
57 57 assert_select 'a[href=?][title=?]',
58 58 'https://mydomain.foo/issues/1',
59 "Cannot print recipes (New)",
59 "Bug: Cannot print recipes (New)",
60 60 :text => '#1'
61 61 # link to a changeset
62 62 assert_select 'a[href=?][title=?]',
63 63 'https://mydomain.foo/projects/ecookbook/repository/revisions/2',
64 64 'This commit fixes #1, #2 and references #1 & #3',
65 65 :text => 'r2'
66 66 # link to a description diff
67 67 assert_select 'a[href^=?][title=?]',
68 68 # should be https://mydomain.foo/journals/diff/3?detail_id=4
69 69 # but the Rails 4.2 DOM assertion doesn't handle the ? in the
70 70 # attribute value
71 71 'https://mydomain.foo/journals/diff/3',
72 72 'View differences',
73 73 :text => 'diff'
74 74 # link to an attachment
75 75 assert_select 'a[href=?]',
76 76 'https://mydomain.foo/attachments/download/4/source.rb',
77 77 :text => 'source.rb'
78 78 end
79 79 end
80 80
81 81 def test_generated_links_with_prefix
82 82 relative_url_root = Redmine::Utils.relative_url_root
83 83 Setting.host_name = 'mydomain.foo/rdm'
84 84 Setting.protocol = 'http'
85 85
86 86 journal = Journal.find(3)
87 87 assert Mailer.deliver_issue_edit(journal)
88 88
89 89 mail = last_email
90 90 assert_not_nil mail
91 91
92 92 assert_select_email do
93 93 # link to the main ticket
94 94 assert_select 'a[href=?]',
95 95 'http://mydomain.foo/rdm/issues/2#change-3',
96 96 :text => 'Feature request #2: Add ingredients categories'
97 97 # link to a referenced ticket
98 98 assert_select 'a[href=?][title=?]',
99 99 'http://mydomain.foo/rdm/issues/1',
100 "Cannot print recipes (New)",
100 "Bug: Cannot print recipes (New)",
101 101 :text => '#1'
102 102 # link to a changeset
103 103 assert_select 'a[href=?][title=?]',
104 104 'http://mydomain.foo/rdm/projects/ecookbook/repository/revisions/2',
105 105 'This commit fixes #1, #2 and references #1 & #3',
106 106 :text => 'r2'
107 107 # link to a description diff
108 108 assert_select 'a[href^=?][title=?]',
109 109 # should be http://mydomain.foo/rdm/journals/diff/3?detail_id=4
110 110 # but the Rails 4.2 DOM assertion doesn't handle the ? in the
111 111 # attribute value
112 112 'http://mydomain.foo/rdm/journals/diff/3',
113 113 'View differences',
114 114 :text => 'diff'
115 115 # link to an attachment
116 116 assert_select 'a[href=?]',
117 117 'http://mydomain.foo/rdm/attachments/download/4/source.rb',
118 118 :text => 'source.rb'
119 119 end
120 120 end
121 121
122 122 def test_generated_links_with_port_and_prefix
123 123 with_settings :host_name => '10.0.0.1:81/redmine', :protocol => 'http' do
124 124 Mailer.test_email(User.find(1)).deliver
125 125 mail = last_email
126 126 assert_not_nil mail
127 127 assert_include 'http://10.0.0.1:81/redmine', mail_body(mail)
128 128 end
129 129 end
130 130
131 131 def test_generated_links_with_port
132 132 with_settings :host_name => '10.0.0.1:81', :protocol => 'http' do
133 133 Mailer.test_email(User.find(1)).deliver
134 134 mail = last_email
135 135 assert_not_nil mail
136 136 assert_include 'http://10.0.0.1:81', mail_body(mail)
137 137 end
138 138 end
139 139
140 140 def test_issue_edit_should_generate_url_with_hostname_for_relations
141 141 journal = Journal.new(:journalized => Issue.find(1), :user => User.find(1), :created_on => Time.now)
142 142 journal.details << JournalDetail.new(:property => 'relation', :prop_key => 'label_relates_to', :value => 2)
143 143 Mailer.deliver_issue_edit(journal)
144 144 assert_not_nil last_email
145 145 assert_select_email do
146 146 assert_select 'a[href=?]', 'http://mydomain.foo/issues/2', :text => 'Feature request #2'
147 147 end
148 148 end
149 149
150 150 def test_generated_links_with_prefix_and_no_relative_url_root
151 151 relative_url_root = Redmine::Utils.relative_url_root
152 152 Setting.host_name = 'mydomain.foo/rdm'
153 153 Setting.protocol = 'http'
154 154 Redmine::Utils.relative_url_root = nil
155 155
156 156 journal = Journal.find(3)
157 157 assert Mailer.deliver_issue_edit(journal)
158 158
159 159 mail = last_email
160 160 assert_not_nil mail
161 161
162 162 assert_select_email do
163 163 # link to the main ticket
164 164 assert_select 'a[href=?]',
165 165 'http://mydomain.foo/rdm/issues/2#change-3',
166 166 :text => 'Feature request #2: Add ingredients categories'
167 167 # link to a referenced ticket
168 168 assert_select 'a[href=?][title=?]',
169 169 'http://mydomain.foo/rdm/issues/1',
170 "Cannot print recipes (New)",
170 "Bug: Cannot print recipes (New)",
171 171 :text => '#1'
172 172 # link to a changeset
173 173 assert_select 'a[href=?][title=?]',
174 174 'http://mydomain.foo/rdm/projects/ecookbook/repository/revisions/2',
175 175 'This commit fixes #1, #2 and references #1 & #3',
176 176 :text => 'r2'
177 177 # link to a description diff
178 178 assert_select 'a[href^=?][title=?]',
179 179 # should be http://mydomain.foo/rdm/journals/diff/3?detail_id=4
180 180 # but the Rails 4.2 DOM assertion doesn't handle the ? in the
181 181 # attribute value
182 182 'http://mydomain.foo/rdm/journals/diff/3',
183 183 'View differences',
184 184 :text => 'diff'
185 185 # link to an attachment
186 186 assert_select 'a[href=?]',
187 187 'http://mydomain.foo/rdm/attachments/download/4/source.rb',
188 188 :text => 'source.rb'
189 189 end
190 190 ensure
191 191 # restore it
192 192 Redmine::Utils.relative_url_root = relative_url_root
193 193 end
194 194
195 195 def test_email_headers
196 196 issue = Issue.find(1)
197 197 Mailer.deliver_issue_add(issue)
198 198 mail = last_email
199 199 assert_not_nil mail
200 200 assert_equal 'All', mail.header['X-Auto-Response-Suppress'].to_s
201 201 assert_equal 'auto-generated', mail.header['Auto-Submitted'].to_s
202 202 assert_equal '<redmine.example.net>', mail.header['List-Id'].to_s
203 203 end
204 204
205 205 def test_email_headers_should_include_sender
206 206 issue = Issue.find(1)
207 207 Mailer.deliver_issue_add(issue)
208 208 mail = last_email
209 209 assert_equal issue.author.login, mail.header['X-Redmine-Sender'].to_s
210 210 end
211 211
212 212 def test_plain_text_mail
213 213 Setting.plain_text_mail = 1
214 214 journal = Journal.find(2)
215 215 Mailer.deliver_issue_edit(journal)
216 216 mail = last_email
217 217 assert_equal "text/plain; charset=UTF-8", mail.content_type
218 218 assert_equal 0, mail.parts.size
219 219 assert !mail.encoded.include?('href')
220 220 end
221 221
222 222 def test_html_mail
223 223 Setting.plain_text_mail = 0
224 224 journal = Journal.find(2)
225 225 Mailer.deliver_issue_edit(journal)
226 226 mail = last_email
227 227 assert_equal 2, mail.parts.size
228 228 assert mail.encoded.include?('href')
229 229 end
230 230
231 231 def test_from_header
232 232 with_settings :mail_from => 'redmine@example.net' do
233 233 Mailer.test_email(User.find(1)).deliver
234 234 end
235 235 mail = last_email
236 236 assert_equal 'redmine@example.net', mail.from_addrs.first
237 237 end
238 238
239 239 def test_from_header_with_phrase
240 240 with_settings :mail_from => 'Redmine app <redmine@example.net>' do
241 241 Mailer.test_email(User.find(1)).deliver
242 242 end
243 243 mail = last_email
244 244 assert_equal 'redmine@example.net', mail.from_addrs.first
245 245 assert_equal 'Redmine app <redmine@example.net>', mail.header['From'].to_s
246 246 end
247 247
248 248 def test_should_not_send_email_without_recipient
249 249 news = News.first
250 250 user = news.author
251 251 # Remove members except news author
252 252 news.project.memberships.each {|m| m.destroy unless m.user == user}
253 253
254 254 user.pref.no_self_notified = false
255 255 user.pref.save
256 256 User.current = user
257 257 Mailer.news_added(news.reload).deliver
258 258 assert_equal 1, last_email.bcc.size
259 259
260 260 # nobody to notify
261 261 user.pref.no_self_notified = true
262 262 user.pref.save
263 263 User.current = user
264 264 ActionMailer::Base.deliveries.clear
265 265 Mailer.news_added(news.reload).deliver
266 266 assert ActionMailer::Base.deliveries.empty?
267 267 end
268 268
269 269 def test_issue_add_message_id
270 270 issue = Issue.find(2)
271 271 Mailer.deliver_issue_add(issue)
272 272 mail = last_email
273 273 assert_match /^redmine\.issue-2\.20060719190421\.[a-f0-9]+@example\.net/, mail.message_id
274 274 assert_include "redmine.issue-2.20060719190421@example.net", mail.references
275 275 end
276 276
277 277 def test_issue_edit_message_id
278 278 journal = Journal.find(3)
279 279 journal.issue = Issue.find(2)
280 280
281 281 Mailer.deliver_issue_edit(journal)
282 282 mail = last_email
283 283 assert_match /^redmine\.journal-3\.\d+\.[a-f0-9]+@example\.net/, mail.message_id
284 284 assert_include "redmine.issue-2.20060719190421@example.net", mail.references
285 285 assert_select_email do
286 286 # link to the update
287 287 assert_select "a[href=?]",
288 288 "http://mydomain.foo/issues/#{journal.journalized_id}#change-#{journal.id}"
289 289 end
290 290 end
291 291
292 292 def test_message_posted_message_id
293 293 message = Message.find(1)
294 294 Mailer.message_posted(message).deliver
295 295 mail = last_email
296 296 assert_match /^redmine\.message-1\.\d+\.[a-f0-9]+@example\.net/, mail.message_id
297 297 assert_include "redmine.message-1.20070512151532@example.net", mail.references
298 298 assert_select_email do
299 299 # link to the message
300 300 assert_select "a[href=?]",
301 301 "http://mydomain.foo/boards/#{message.board.id}/topics/#{message.id}",
302 302 :text => message.subject
303 303 end
304 304 end
305 305
306 306 def test_reply_posted_message_id
307 307 message = Message.find(3)
308 308 Mailer.message_posted(message).deliver
309 309 mail = last_email
310 310 assert_match /^redmine\.message-3\.\d+\.[a-f0-9]+@example\.net/, mail.message_id
311 311 assert_include "redmine.message-1.20070512151532@example.net", mail.references
312 312 assert_select_email do
313 313 # link to the reply
314 314 assert_select "a[href=?]",
315 315 "http://mydomain.foo/boards/#{message.board.id}/topics/#{message.root.id}?r=#{message.id}#message-#{message.id}",
316 316 :text => message.subject
317 317 end
318 318 end
319 319
320 320 test "#issue_add should notify project members" do
321 321 issue = Issue.find(1)
322 322 assert Mailer.deliver_issue_add(issue)
323 323 assert last_email.bcc.include?('dlopper@somenet.foo')
324 324 end
325 325
326 326 def test_issue_add_should_send_mail_to_all_user_email_address
327 327 EmailAddress.create!(:user_id => 3, :address => 'otheremail@somenet.foo')
328 328 issue = Issue.find(1)
329 329 assert Mailer.deliver_issue_add(issue)
330 330 assert last_email.bcc.include?('dlopper@somenet.foo')
331 331 assert last_email.bcc.include?('otheremail@somenet.foo')
332 332 end
333 333
334 334 test "#issue_add should not notify project members that are not allow to view the issue" do
335 335 issue = Issue.find(1)
336 336 Role.find(2).remove_permission!(:view_issues)
337 337 assert Mailer.deliver_issue_add(issue)
338 338 assert !last_email.bcc.include?('dlopper@somenet.foo')
339 339 end
340 340
341 341 test "#issue_add should notify issue watchers" do
342 342 issue = Issue.find(1)
343 343 user = User.find(9)
344 344 # minimal email notification options
345 345 user.pref.no_self_notified = '1'
346 346 user.pref.save
347 347 user.mail_notification = false
348 348 user.save
349 349
350 350 Watcher.create!(:watchable => issue, :user => user)
351 351 assert Mailer.deliver_issue_add(issue)
352 352 assert last_email.bcc.include?(user.mail)
353 353 end
354 354
355 355 test "#issue_add should not notify watchers not allowed to view the issue" do
356 356 issue = Issue.find(1)
357 357 user = User.find(9)
358 358 Watcher.create!(:watchable => issue, :user => user)
359 359 Role.non_member.remove_permission!(:view_issues)
360 360 assert Mailer.deliver_issue_add(issue)
361 361 assert !last_email.bcc.include?(user.mail)
362 362 end
363 363
364 364 def test_issue_add_should_include_enabled_fields
365 365 issue = Issue.find(2)
366 366 assert Mailer.deliver_issue_add(issue)
367 367 assert_mail_body_match '* Target version: 1.0', last_email
368 368 assert_select_email do
369 369 assert_select 'li', :text => 'Target version: 1.0'
370 370 end
371 371 end
372 372
373 373 def test_issue_add_should_not_include_disabled_fields
374 374 issue = Issue.find(2)
375 375 tracker = issue.tracker
376 376 tracker.core_fields -= ['fixed_version_id']
377 377 tracker.save!
378 378 assert Mailer.deliver_issue_add(issue)
379 379 assert_mail_body_no_match 'Target version', last_email
380 380 assert_select_email do
381 381 assert_select 'li', :text => /Target version/, :count => 0
382 382 end
383 383 end
384 384
385 385 # test mailer methods for each language
386 386 def test_issue_add
387 387 issue = Issue.find(1)
388 388 with_each_language_as_default do
389 389 assert Mailer.deliver_issue_add(issue)
390 390 end
391 391 end
392 392
393 393 def test_issue_edit
394 394 journal = Journal.find(1)
395 395 with_each_language_as_default do
396 396 assert Mailer.deliver_issue_edit(journal)
397 397 end
398 398 end
399 399
400 400 def test_issue_edit_should_send_private_notes_to_users_with_permission_only
401 401 journal = Journal.find(1)
402 402 journal.private_notes = true
403 403 journal.save!
404 404
405 405 Role.find(2).add_permission! :view_private_notes
406 406 Mailer.deliver_issue_edit(journal)
407 407 assert_equal %w(dlopper@somenet.foo jsmith@somenet.foo), ActionMailer::Base.deliveries.last.bcc.sort
408 408
409 409 Role.find(2).remove_permission! :view_private_notes
410 410 Mailer.deliver_issue_edit(journal)
411 411 assert_equal %w(jsmith@somenet.foo), ActionMailer::Base.deliveries.last.bcc.sort
412 412 end
413 413
414 414 def test_issue_edit_should_send_private_notes_to_watchers_with_permission_only
415 415 Issue.find(1).set_watcher(User.find_by_login('someone'))
416 416 journal = Journal.find(1)
417 417 journal.private_notes = true
418 418 journal.save!
419 419
420 420 Role.non_member.add_permission! :view_private_notes
421 421 Mailer.deliver_issue_edit(journal)
422 422 assert_include 'someone@foo.bar', ActionMailer::Base.deliveries.last.bcc.sort
423 423
424 424 Role.non_member.remove_permission! :view_private_notes
425 425 Mailer.deliver_issue_edit(journal)
426 426 assert_not_include 'someone@foo.bar', ActionMailer::Base.deliveries.last.bcc.sort
427 427 end
428 428
429 429 def test_issue_edit_should_mark_private_notes
430 430 journal = Journal.find(2)
431 431 journal.private_notes = true
432 432 journal.save!
433 433
434 434 with_settings :default_language => 'en' do
435 435 Mailer.deliver_issue_edit(journal)
436 436 end
437 437 assert_mail_body_match '(Private notes)', last_email
438 438 end
439 439
440 440 def test_issue_edit_with_relation_should_notify_users_who_can_see_the_related_issue
441 441 issue = Issue.generate!
442 442 issue.init_journal(User.find(1))
443 443 private_issue = Issue.generate!(:is_private => true)
444 444 IssueRelation.create!(:issue_from => issue, :issue_to => private_issue, :relation_type => 'relates')
445 445 issue.reload
446 446 assert_equal 1, issue.journals.size
447 447 journal = issue.journals.first
448 448 ActionMailer::Base.deliveries.clear
449 449
450 450 Mailer.deliver_issue_edit(journal)
451 451 last_email.bcc.each do |email|
452 452 user = User.find_by_mail(email)
453 453 assert private_issue.visible?(user), "Issue was not visible to #{user}"
454 454 end
455 455 end
456 456
457 457 def test_document_added
458 458 document = Document.find(1)
459 459 with_each_language_as_default do
460 460 assert Mailer.document_added(document).deliver
461 461 end
462 462 end
463 463
464 464 def test_attachments_added
465 465 attachements = [ Attachment.find_by_container_type('Document') ]
466 466 with_each_language_as_default do
467 467 assert Mailer.attachments_added(attachements).deliver
468 468 end
469 469 end
470 470
471 471 def test_version_file_added
472 472 attachements = [ Attachment.find_by_container_type('Version') ]
473 473 assert Mailer.attachments_added(attachements).deliver
474 474 assert_not_nil last_email.bcc
475 475 assert last_email.bcc.any?
476 476 assert_select_email do
477 477 assert_select "a[href=?]", "http://mydomain.foo/projects/ecookbook/files"
478 478 end
479 479 end
480 480
481 481 def test_project_file_added
482 482 attachements = [ Attachment.find_by_container_type('Project') ]
483 483 assert Mailer.attachments_added(attachements).deliver
484 484 assert_not_nil last_email.bcc
485 485 assert last_email.bcc.any?
486 486 assert_select_email do
487 487 assert_select "a[href=?]", "http://mydomain.foo/projects/ecookbook/files"
488 488 end
489 489 end
490 490
491 491 def test_news_added
492 492 news = News.first
493 493 with_each_language_as_default do
494 494 assert Mailer.news_added(news).deliver
495 495 end
496 496 end
497 497
498 498 def test_news_added_should_notify_project_news_watchers
499 499 user1 = User.generate!
500 500 user2 = User.generate!
501 501 news = News.find(1)
502 502 news.project.enabled_module('news').add_watcher(user1)
503 503
504 504 Mailer.news_added(news).deliver
505 505 assert_include user1.mail, last_email.bcc
506 506 assert_not_include user2.mail, last_email.bcc
507 507 end
508 508
509 509 def test_news_comment_added
510 510 comment = Comment.find(2)
511 511 with_each_language_as_default do
512 512 assert Mailer.news_comment_added(comment).deliver
513 513 end
514 514 end
515 515
516 516 def test_message_posted
517 517 message = Message.first
518 518 recipients = ([message.root] + message.root.children).collect {|m| m.author.mail if m.author}
519 519 recipients = recipients.compact.uniq
520 520 with_each_language_as_default do
521 521 assert Mailer.message_posted(message).deliver
522 522 end
523 523 end
524 524
525 525 def test_wiki_content_added
526 526 content = WikiContent.find(1)
527 527 with_each_language_as_default do
528 528 assert_difference 'ActionMailer::Base.deliveries.size' do
529 529 assert Mailer.wiki_content_added(content).deliver
530 530 assert_select_email do
531 531 assert_select 'a[href=?]',
532 532 'http://mydomain.foo/projects/ecookbook/wiki/CookBook_documentation',
533 533 :text => 'CookBook documentation'
534 534 end
535 535 end
536 536 end
537 537 end
538 538
539 539 def test_wiki_content_updated
540 540 content = WikiContent.find(1)
541 541 with_each_language_as_default do
542 542 assert_difference 'ActionMailer::Base.deliveries.size' do
543 543 assert Mailer.wiki_content_updated(content).deliver
544 544 assert_select_email do
545 545 assert_select 'a[href=?]',
546 546 'http://mydomain.foo/projects/ecookbook/wiki/CookBook_documentation',
547 547 :text => 'CookBook documentation'
548 548 end
549 549 end
550 550 end
551 551 end
552 552
553 553 def test_account_information
554 554 user = User.find(2)
555 555 valid_languages.each do |lang|
556 556 user.update_attribute :language, lang.to_s
557 557 user.reload
558 558 assert Mailer.account_information(user, 'pAsswORd').deliver
559 559 end
560 560 end
561 561
562 562 def test_lost_password
563 563 token = Token.find(2)
564 564 valid_languages.each do |lang|
565 565 token.user.update_attribute :language, lang.to_s
566 566 token.reload
567 567 assert Mailer.lost_password(token).deliver
568 568 end
569 569 end
570 570
571 571 def test_register
572 572 token = Token.find(1)
573 573 Setting.host_name = 'redmine.foo'
574 574 Setting.protocol = 'https'
575 575
576 576 valid_languages.each do |lang|
577 577 token.user.update_attribute :language, lang.to_s
578 578 token.reload
579 579 ActionMailer::Base.deliveries.clear
580 580 assert Mailer.register(token).deliver
581 581 mail = last_email
582 582 assert_select_email do
583 583 assert_select "a[href=?]",
584 584 "https://redmine.foo/account/activate?token=#{token.value}",
585 585 :text => "https://redmine.foo/account/activate?token=#{token.value}"
586 586 end
587 587 end
588 588 end
589 589
590 590 def test_test
591 591 user = User.find(1)
592 592 valid_languages.each do |lang|
593 593 user.update_attribute :language, lang.to_s
594 594 assert Mailer.test_email(user).deliver
595 595 end
596 596 end
597 597
598 598 def test_reminders
599 599 Mailer.reminders(:days => 42)
600 600 assert_equal 1, ActionMailer::Base.deliveries.size
601 601 mail = last_email
602 602 assert mail.bcc.include?('dlopper@somenet.foo')
603 603 assert_mail_body_match 'Bug #3: Error 281 when updating a recipe', mail
604 604 assert_equal '1 issue(s) due in the next 42 days', mail.subject
605 605 end
606 606
607 607 def test_reminders_should_not_include_closed_issues
608 608 with_settings :default_language => 'en' do
609 609 Issue.create!(:project_id => 1, :tracker_id => 1, :status_id => 5,
610 610 :subject => 'Closed issue', :assigned_to_id => 3,
611 611 :due_date => 5.days.from_now,
612 612 :author_id => 2)
613 613 ActionMailer::Base.deliveries.clear
614 614
615 615 Mailer.reminders(:days => 42)
616 616 assert_equal 1, ActionMailer::Base.deliveries.size
617 617 mail = last_email
618 618 assert mail.bcc.include?('dlopper@somenet.foo')
619 619 assert_mail_body_no_match 'Closed issue', mail
620 620 end
621 621 end
622 622
623 623 def test_reminders_for_users
624 624 Mailer.reminders(:days => 42, :users => ['5'])
625 625 assert_equal 0, ActionMailer::Base.deliveries.size # No mail for dlopper
626 626 Mailer.reminders(:days => 42, :users => ['3'])
627 627 assert_equal 1, ActionMailer::Base.deliveries.size # No mail for dlopper
628 628 mail = last_email
629 629 assert mail.bcc.include?('dlopper@somenet.foo')
630 630 assert_mail_body_match 'Bug #3: Error 281 when updating a recipe', mail
631 631 end
632 632
633 633 def test_reminder_should_include_issues_assigned_to_groups
634 634 with_settings :default_language => 'en' do
635 635 group = Group.generate!
636 636 group.users << User.find(2)
637 637 group.users << User.find(3)
638 638
639 639 Issue.create!(:project_id => 1, :tracker_id => 1, :status_id => 1,
640 640 :subject => 'Assigned to group', :assigned_to => group,
641 641 :due_date => 5.days.from_now,
642 642 :author_id => 2)
643 643 ActionMailer::Base.deliveries.clear
644 644
645 645 Mailer.reminders(:days => 7)
646 646 assert_equal 2, ActionMailer::Base.deliveries.size
647 647 assert_equal %w(dlopper@somenet.foo jsmith@somenet.foo), ActionMailer::Base.deliveries.map(&:bcc).flatten.sort
648 648 ActionMailer::Base.deliveries.each do |mail|
649 649 assert_mail_body_match 'Assigned to group', mail
650 650 end
651 651 end
652 652 end
653 653
654 654 def test_reminders_with_version_option
655 655 with_settings :default_language => 'en' do
656 656 version = Version.generate!(:name => 'Acme', :project_id => 1)
657 657 Issue.generate!(:assigned_to => User.find(2), :due_date => 5.days.from_now)
658 658 Issue.generate!(:assigned_to => User.find(3), :due_date => 5.days.from_now, :fixed_version => version)
659 659 ActionMailer::Base.deliveries.clear
660 660
661 661 Mailer.reminders(:days => 42, :version => 'acme')
662 662 assert_equal 1, ActionMailer::Base.deliveries.size
663 663
664 664 mail = last_email
665 665 assert mail.bcc.include?('dlopper@somenet.foo')
666 666 end
667 667 end
668 668
669 669 def test_mailer_should_not_change_locale
670 670 # Set current language to italian
671 671 set_language_if_valid 'it'
672 672 # Send an email to a french user
673 673 user = User.find(1)
674 674 user.language = 'fr'
675 675 Mailer.account_activated(user).deliver
676 676 mail = last_email
677 677 assert_mail_body_match 'Votre compte', mail
678 678
679 679 assert_equal :it, current_language
680 680 end
681 681
682 682 def test_with_deliveries_off
683 683 Mailer.with_deliveries false do
684 684 Mailer.test_email(User.find(1)).deliver
685 685 end
686 686 assert ActionMailer::Base.deliveries.empty?
687 687 # should restore perform_deliveries
688 688 assert ActionMailer::Base.perform_deliveries
689 689 end
690 690
691 691 def test_token_for_should_strip_trailing_gt_from_address_with_full_name
692 692 with_settings :mail_from => "Redmine Mailer<no-reply@redmine.org>" do
693 693 assert_match /\Aredmine.issue-\d+\.\d+\.[0-9a-f]+@redmine.org\z/, Mailer.token_for(Issue.generate!)
694 694 end
695 695 end
696 696
697 697 def test_layout_should_include_the_emails_header
698 698 with_settings :emails_header => "*Header content*" do
699 699 with_settings :plain_text_mail => 0 do
700 700 assert Mailer.test_email(User.find(1)).deliver
701 701 assert_select_email do
702 702 assert_select ".header" do
703 703 assert_select "strong", :text => "Header content"
704 704 end
705 705 end
706 706 end
707 707 with_settings :plain_text_mail => 1 do
708 708 assert Mailer.test_email(User.find(1)).deliver
709 709 mail = last_email
710 710 assert_not_nil mail
711 711 assert_include "*Header content*", mail.body.decoded
712 712 end
713 713 end
714 714 end
715 715
716 716 def test_layout_should_not_include_empty_emails_header
717 717 with_settings :emails_header => "", :plain_text_mail => 0 do
718 718 assert Mailer.test_email(User.find(1)).deliver
719 719 assert_select_email do
720 720 assert_select ".header", false
721 721 end
722 722 end
723 723 end
724 724
725 725 def test_layout_should_include_the_emails_footer
726 726 with_settings :emails_footer => "*Footer content*" do
727 727 with_settings :plain_text_mail => 0 do
728 728 assert Mailer.test_email(User.find(1)).deliver
729 729 assert_select_email do
730 730 assert_select ".footer" do
731 731 assert_select "strong", :text => "Footer content"
732 732 end
733 733 end
734 734 end
735 735 with_settings :plain_text_mail => 1 do
736 736 assert Mailer.test_email(User.find(1)).deliver
737 737 mail = last_email
738 738 assert_not_nil mail
739 739 assert_include "\n-- \n", mail.body.decoded
740 740 assert_include "*Footer content*", mail.body.decoded
741 741 end
742 742 end
743 743 end
744 744
745 745 def test_layout_should_not_include_empty_emails_footer
746 746 with_settings :emails_footer => "" do
747 747 with_settings :plain_text_mail => 0 do
748 748 assert Mailer.test_email(User.find(1)).deliver
749 749 assert_select_email do
750 750 assert_select ".footer", false
751 751 end
752 752 end
753 753 with_settings :plain_text_mail => 1 do
754 754 assert Mailer.test_email(User.find(1)).deliver
755 755 mail = last_email
756 756 assert_not_nil mail
757 757 assert_not_include "\n-- \n", mail.body.decoded
758 758 end
759 759 end
760 760 end
761 761
762 762 def test_should_escape_html_templates_only
763 763 Issue.generate!(:project_id => 1, :tracker_id => 1, :subject => 'Subject with a <tag>')
764 764 mail = last_email
765 765 assert_equal 2, mail.parts.size
766 766 assert_include '<tag>', text_part.body.encoded
767 767 assert_include '&lt;tag&gt;', html_part.body.encoded
768 768 end
769 769
770 770 def test_should_raise_delivery_errors_when_raise_delivery_errors_is_true
771 771 mail = Mailer.test_email(User.find(1))
772 772 mail.delivery_method.stubs(:deliver!).raises(Exception.new("delivery error"))
773 773
774 774 ActionMailer::Base.raise_delivery_errors = true
775 775 assert_raise Exception, "delivery error" do
776 776 mail.deliver
777 777 end
778 778 ensure
779 779 ActionMailer::Base.raise_delivery_errors = false
780 780 end
781 781
782 782 def test_should_log_delivery_errors_when_raise_delivery_errors_is_false
783 783 mail = Mailer.test_email(User.find(1))
784 784 mail.delivery_method.stubs(:deliver!).raises(Exception.new("delivery error"))
785 785
786 786 Rails.logger.expects(:error).with("Email delivery error: delivery error")
787 787 ActionMailer::Base.raise_delivery_errors = false
788 788 assert_nothing_raised do
789 789 mail.deliver
790 790 end
791 791 end
792 792
793 793 def test_with_synched_deliveries_should_yield_with_synced_deliveries
794 794 ActionMailer::Base.delivery_method = :async_smtp
795 795 ActionMailer::Base.async_smtp_settings = {:foo => 'bar'}
796 796
797 797 Mailer.with_synched_deliveries do
798 798 assert_equal :smtp, ActionMailer::Base.delivery_method
799 799 assert_equal({:foo => 'bar'}, ActionMailer::Base.smtp_settings)
800 800 end
801 801 assert_equal :async_smtp, ActionMailer::Base.delivery_method
802 802 ensure
803 803 ActionMailer::Base.delivery_method = :test
804 804 end
805 805
806 806 def test_email_addresses_should_keep_addresses
807 807 assert_equal ["foo@example.net"],
808 808 Mailer.email_addresses("foo@example.net")
809 809
810 810 assert_equal ["foo@example.net", "bar@example.net"],
811 811 Mailer.email_addresses(["foo@example.net", "bar@example.net"])
812 812 end
813 813
814 814 def test_email_addresses_should_replace_users_with_their_email_addresses
815 815 assert_equal ["admin@somenet.foo"],
816 816 Mailer.email_addresses(User.find(1))
817 817
818 818 assert_equal ["admin@somenet.foo", "jsmith@somenet.foo"],
819 819 Mailer.email_addresses(User.where(:id => [1,2])).sort
820 820 end
821 821
822 822 def test_email_addresses_should_include_notified_emails_addresses_only
823 823 EmailAddress.create!(:user_id => 2, :address => "another@somenet.foo", :notify => false)
824 824 EmailAddress.create!(:user_id => 2, :address => "another2@somenet.foo")
825 825
826 826 assert_equal ["another2@somenet.foo", "jsmith@somenet.foo"],
827 827 Mailer.email_addresses(User.find(2)).sort
828 828 end
829 829
830 830 private
831 831
832 832 def last_email
833 833 mail = ActionMailer::Base.deliveries.last
834 834 assert_not_nil mail
835 835 mail
836 836 end
837 837
838 838 def text_part
839 839 last_email.parts.detect {|part| part.content_type.include?('text/plain')}
840 840 end
841 841
842 842 def html_part
843 843 last_email.parts.detect {|part| part.content_type.include?('text/html')}
844 844 end
845 845
846 846 def with_each_language_as_default(&block)
847 847 valid_languages.each do |lang|
848 848 with_settings :default_language => lang.to_s do
849 849 yield lang
850 850 end
851 851 end
852 852 end
853 853 end
General Comments 0
You need to be logged in to leave comments. Login now