##// END OF EJS Templates
Fixed: partial toc when text contains pre tags (#7172)....
Jean-Philippe Lang -
r4464:af7006dff6f8
parent child
Show More
@@ -1,907 +1,919
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2010 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 'forwardable'
19 19 require 'cgi'
20 20
21 21 module ApplicationHelper
22 22 include Redmine::WikiFormatting::Macros::Definitions
23 23 include Redmine::I18n
24 24 include GravatarHelper::PublicMethods
25 25
26 26 extend Forwardable
27 27 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
28 28
29 29 # Return true if user is authorized for controller/action, otherwise false
30 30 def authorize_for(controller, action)
31 31 User.current.allowed_to?({:controller => controller, :action => action}, @project)
32 32 end
33 33
34 34 # Display a link if user is authorized
35 35 #
36 36 # @param [String] name Anchor text (passed to link_to)
37 37 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
38 38 # @param [optional, Hash] html_options Options passed to link_to
39 39 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
40 40 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
41 41 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
42 42 end
43 43
44 44 # Display a link to remote if user is authorized
45 45 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
46 46 url = options[:url] || {}
47 47 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
48 48 end
49 49
50 50 # Displays a link to user's account page if active
51 51 def link_to_user(user, options={})
52 52 if user.is_a?(User)
53 53 name = h(user.name(options[:format]))
54 54 if user.active?
55 55 link_to name, :controller => 'users', :action => 'show', :id => user
56 56 else
57 57 name
58 58 end
59 59 else
60 60 h(user.to_s)
61 61 end
62 62 end
63 63
64 64 # Displays a link to +issue+ with its subject.
65 65 # Examples:
66 66 #
67 67 # link_to_issue(issue) # => Defect #6: This is the subject
68 68 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
69 69 # link_to_issue(issue, :subject => false) # => Defect #6
70 70 # link_to_issue(issue, :project => true) # => Foo - Defect #6
71 71 #
72 72 def link_to_issue(issue, options={})
73 73 title = nil
74 74 subject = nil
75 75 if options[:subject] == false
76 76 title = truncate(issue.subject, :length => 60)
77 77 else
78 78 subject = issue.subject
79 79 if options[:truncate]
80 80 subject = truncate(subject, :length => options[:truncate])
81 81 end
82 82 end
83 83 s = link_to "#{issue.tracker} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
84 84 :class => issue.css_classes,
85 85 :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 action = options.delete(:download) ? 'download' : 'show'
98 98
99 99 link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
100 100 end
101 101
102 102 # Generates a link to a SCM revision
103 103 # Options:
104 104 # * :text - Link text (default to the formatted revision)
105 105 def link_to_revision(revision, project, options={})
106 106 text = options.delete(:text) || format_revision(revision)
107 107
108 108 link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => revision}, :title => l(:label_revision_id, revision))
109 109 end
110 110
111 111 # Generates a link to a project if active
112 112 # Examples:
113 113 #
114 114 # link_to_project(project) # => link to the specified project overview
115 115 # link_to_project(project, :action=>'settings') # => link to project settings
116 116 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
117 117 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
118 118 #
119 119 def link_to_project(project, options={}, html_options = nil)
120 120 if project.active?
121 121 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
122 122 link_to(h(project), url, html_options)
123 123 else
124 124 h(project)
125 125 end
126 126 end
127 127
128 128 def toggle_link(name, id, options={})
129 129 onclick = "Element.toggle('#{id}'); "
130 130 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
131 131 onclick << "return false;"
132 132 link_to(name, "#", :onclick => onclick)
133 133 end
134 134
135 135 def image_to_function(name, function, html_options = {})
136 136 html_options.symbolize_keys!
137 137 tag(:input, html_options.merge({
138 138 :type => "image", :src => image_path(name),
139 139 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
140 140 }))
141 141 end
142 142
143 143 def prompt_to_remote(name, text, param, url, html_options = {})
144 144 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
145 145 link_to name, {}, html_options
146 146 end
147 147
148 148 def format_activity_title(text)
149 149 h(truncate_single_line(text, :length => 100))
150 150 end
151 151
152 152 def format_activity_day(date)
153 153 date == Date.today ? l(:label_today).titleize : format_date(date)
154 154 end
155 155
156 156 def format_activity_description(text)
157 157 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
158 158 end
159 159
160 160 def format_version_name(version)
161 161 if version.project == @project
162 162 h(version)
163 163 else
164 164 h("#{version.project} - #{version}")
165 165 end
166 166 end
167 167
168 168 def due_date_distance_in_words(date)
169 169 if date
170 170 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
171 171 end
172 172 end
173 173
174 174 def render_page_hierarchy(pages, node=nil)
175 175 content = ''
176 176 if pages[node]
177 177 content << "<ul class=\"pages-hierarchy\">\n"
178 178 pages[node].each do |page|
179 179 content << "<li>"
180 180 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
181 181 :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
182 182 content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
183 183 content << "</li>\n"
184 184 end
185 185 content << "</ul>\n"
186 186 end
187 187 content
188 188 end
189 189
190 190 # Renders flash messages
191 191 def render_flash_messages
192 192 s = ''
193 193 flash.each do |k,v|
194 194 s << content_tag('div', v, :class => "flash #{k}")
195 195 end
196 196 s
197 197 end
198 198
199 199 # Renders tabs and their content
200 200 def render_tabs(tabs)
201 201 if tabs.any?
202 202 render :partial => 'common/tabs', :locals => {:tabs => tabs}
203 203 else
204 204 content_tag 'p', l(:label_no_data), :class => "nodata"
205 205 end
206 206 end
207 207
208 208 # Renders the project quick-jump box
209 209 def render_project_jump_box
210 210 # Retrieve them now to avoid a COUNT query
211 211 projects = User.current.projects.all
212 212 if projects.any?
213 213 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
214 214 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
215 215 '<option value="" disabled="disabled">---</option>'
216 216 s << project_tree_options_for_select(projects, :selected => @project) do |p|
217 217 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
218 218 end
219 219 s << '</select>'
220 220 s
221 221 end
222 222 end
223 223
224 224 def project_tree_options_for_select(projects, options = {})
225 225 s = ''
226 226 project_tree(projects) do |project, level|
227 227 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
228 228 tag_options = {:value => project.id}
229 229 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
230 230 tag_options[:selected] = 'selected'
231 231 else
232 232 tag_options[:selected] = nil
233 233 end
234 234 tag_options.merge!(yield(project)) if block_given?
235 235 s << content_tag('option', name_prefix + h(project), tag_options)
236 236 end
237 237 s
238 238 end
239 239
240 240 # Yields the given block for each project with its level in the tree
241 241 #
242 242 # Wrapper for Project#project_tree
243 243 def project_tree(projects, &block)
244 244 Project.project_tree(projects, &block)
245 245 end
246 246
247 247 def project_nested_ul(projects, &block)
248 248 s = ''
249 249 if projects.any?
250 250 ancestors = []
251 251 projects.sort_by(&:lft).each do |project|
252 252 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
253 253 s << "<ul>\n"
254 254 else
255 255 ancestors.pop
256 256 s << "</li>"
257 257 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
258 258 ancestors.pop
259 259 s << "</ul></li>\n"
260 260 end
261 261 end
262 262 s << "<li>"
263 263 s << yield(project).to_s
264 264 ancestors << project
265 265 end
266 266 s << ("</li></ul>\n" * ancestors.size)
267 267 end
268 268 s
269 269 end
270 270
271 271 def principals_check_box_tags(name, principals)
272 272 s = ''
273 273 principals.sort.each do |principal|
274 274 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
275 275 end
276 276 s
277 277 end
278 278
279 279 # Truncates and returns the string as a single line
280 280 def truncate_single_line(string, *args)
281 281 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
282 282 end
283 283
284 284 # Truncates at line break after 250 characters or options[:length]
285 285 def truncate_lines(string, options={})
286 286 length = options[:length] || 250
287 287 if string.to_s =~ /\A(.{#{length}}.*?)$/m
288 288 "#{$1}..."
289 289 else
290 290 string
291 291 end
292 292 end
293 293
294 294 def html_hours(text)
295 295 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
296 296 end
297 297
298 298 def authoring(created, author, options={})
299 299 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created))
300 300 end
301 301
302 302 def time_tag(time)
303 303 text = distance_of_time_in_words(Time.now, time)
304 304 if @project
305 305 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
306 306 else
307 307 content_tag('acronym', text, :title => format_time(time))
308 308 end
309 309 end
310 310
311 311 def syntax_highlight(name, content)
312 312 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
313 313 end
314 314
315 315 def to_path_param(path)
316 316 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
317 317 end
318 318
319 319 def pagination_links_full(paginator, count=nil, options={})
320 320 page_param = options.delete(:page_param) || :page
321 321 per_page_links = options.delete(:per_page_links)
322 322 url_param = params.dup
323 323 # don't reuse query params if filters are present
324 324 url_param.merge!(:fields => nil, :values => nil, :operators => nil) if url_param.delete(:set_filter)
325 325
326 326 html = ''
327 327 if paginator.current.previous
328 328 html << link_to_remote_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
329 329 end
330 330
331 331 html << (pagination_links_each(paginator, options) do |n|
332 332 link_to_remote_content_update(n.to_s, url_param.merge(page_param => n))
333 333 end || '')
334 334
335 335 if paginator.current.next
336 336 html << ' ' + link_to_remote_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next))
337 337 end
338 338
339 339 unless count.nil?
340 340 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
341 341 if per_page_links != false && links = per_page_links(paginator.items_per_page)
342 342 html << " | #{links}"
343 343 end
344 344 end
345 345
346 346 html
347 347 end
348 348
349 349 def per_page_links(selected=nil)
350 350 url_param = params.dup
351 351 url_param.clear if url_param.has_key?(:set_filter)
352 352
353 353 links = Setting.per_page_options_array.collect do |n|
354 354 n == selected ? n : link_to_remote(n, {:update => "content",
355 355 :url => params.dup.merge(:per_page => n),
356 356 :method => :get},
357 357 {:href => url_for(url_param.merge(:per_page => n))})
358 358 end
359 359 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
360 360 end
361 361
362 362 def reorder_links(name, url)
363 363 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), url.merge({"#{name}[move_to]" => 'highest'}), :method => :post, :title => l(:label_sort_highest)) +
364 364 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), url.merge({"#{name}[move_to]" => 'higher'}), :method => :post, :title => l(:label_sort_higher)) +
365 365 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), url.merge({"#{name}[move_to]" => 'lower'}), :method => :post, :title => l(:label_sort_lower)) +
366 366 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), url.merge({"#{name}[move_to]" => 'lowest'}), :method => :post, :title => l(:label_sort_lowest))
367 367 end
368 368
369 369 def breadcrumb(*args)
370 370 elements = args.flatten
371 371 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
372 372 end
373 373
374 374 def other_formats_links(&block)
375 375 concat('<p class="other-formats">' + l(:label_export_to))
376 376 yield Redmine::Views::OtherFormatsBuilder.new(self)
377 377 concat('</p>')
378 378 end
379 379
380 380 def page_header_title
381 381 if @project.nil? || @project.new_record?
382 382 h(Setting.app_title)
383 383 else
384 384 b = []
385 385 ancestors = (@project.root? ? [] : @project.ancestors.visible)
386 386 if ancestors.any?
387 387 root = ancestors.shift
388 388 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
389 389 if ancestors.size > 2
390 390 b << '&#8230;'
391 391 ancestors = ancestors[-2, 2]
392 392 end
393 393 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
394 394 end
395 395 b << h(@project)
396 396 b.join(' &#187; ')
397 397 end
398 398 end
399 399
400 400 def html_title(*args)
401 401 if args.empty?
402 402 title = []
403 403 title << @project.name if @project
404 404 title += @html_title if @html_title
405 405 title << Setting.app_title
406 406 title.select {|t| !t.blank? }.join(' - ')
407 407 else
408 408 @html_title ||= []
409 409 @html_title += args
410 410 end
411 411 end
412 412
413 413 # Returns the theme, controller name, and action as css classes for the
414 414 # HTML body.
415 415 def body_css_classes
416 416 css = []
417 417 if theme = Redmine::Themes.theme(Setting.ui_theme)
418 418 css << 'theme-' + theme.name
419 419 end
420 420
421 421 css << 'controller-' + params[:controller]
422 422 css << 'action-' + params[:action]
423 423 css.join(' ')
424 424 end
425 425
426 426 def accesskey(s)
427 427 Redmine::AccessKeys.key_for s
428 428 end
429 429
430 430 # Formats text according to system settings.
431 431 # 2 ways to call this method:
432 432 # * with a String: textilizable(text, options)
433 433 # * with an object and one of its attribute: textilizable(issue, :description, options)
434 434 def textilizable(*args)
435 435 options = args.last.is_a?(Hash) ? args.pop : {}
436 436 case args.size
437 437 when 1
438 438 obj = options[:object]
439 439 text = args.shift
440 440 when 2
441 441 obj = args.shift
442 442 attr = args.shift
443 443 text = obj.send(attr).to_s
444 444 else
445 445 raise ArgumentError, 'invalid arguments to textilizable'
446 446 end
447 447 return '' if text.blank?
448 448 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
449 449 only_path = options.delete(:only_path) == false ? false : true
450 450
451 451 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }
452
453 parse_non_pre_blocks(text) do |text|
452
453 @parsed_headings = []
454 text = parse_non_pre_blocks(text) do |text|
454 455 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_headings].each do |method_name|
455 456 send method_name, text, project, obj, attr, only_path, options
456 457 end
457 458 end
459
460 if @parsed_headings.any?
461 replace_toc(text, @parsed_headings)
462 end
463
464 text
458 465 end
459 466
460 467 def parse_non_pre_blocks(text)
461 468 s = StringScanner.new(text)
462 469 tags = []
463 470 parsed = ''
464 471 while !s.eos?
465 472 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
466 473 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
467 474 if tags.empty?
468 475 yield text
469 476 end
470 477 parsed << text
471 478 if tag
472 479 if closing
473 480 if tags.last == tag.downcase
474 481 tags.pop
475 482 end
476 483 else
477 484 tags << tag.downcase
478 485 end
479 486 parsed << full_tag
480 487 end
481 488 end
482 489 # Close any non closing tags
483 490 while tag = tags.pop
484 491 parsed << "</#{tag}>"
485 492 end
486 493 parsed
487 494 end
488 495
489 496 def parse_inline_attachments(text, project, obj, attr, only_path, options)
490 497 # when using an image link, try to use an attachment, if possible
491 498 if options[:attachments] || (obj && obj.respond_to?(:attachments))
492 499 attachments = nil
493 500 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
494 501 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
495 502 attachments ||= (options[:attachments] || obj.attachments).sort_by(&:created_on).reverse
496 503 # search for the picture in attachments
497 504 if found = attachments.detect { |att| att.filename.downcase == filename }
498 505 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
499 506 desc = found.description.to_s.gsub('"', '')
500 507 if !desc.blank? && alttext.blank?
501 508 alt = " title=\"#{desc}\" alt=\"#{desc}\""
502 509 end
503 510 "src=\"#{image_url}\"#{alt}"
504 511 else
505 512 m
506 513 end
507 514 end
508 515 end
509 516 end
510 517
511 518 # Wiki links
512 519 #
513 520 # Examples:
514 521 # [[mypage]]
515 522 # [[mypage|mytext]]
516 523 # wiki links can refer other project wikis, using project name or identifier:
517 524 # [[project:]] -> wiki starting page
518 525 # [[project:|mytext]]
519 526 # [[project:mypage]]
520 527 # [[project:mypage|mytext]]
521 528 def parse_wiki_links(text, project, obj, attr, only_path, options)
522 529 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
523 530 link_project = project
524 531 esc, all, page, title = $1, $2, $3, $5
525 532 if esc.nil?
526 533 if page =~ /^([^\:]+)\:(.*)$/
527 534 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
528 535 page = $2
529 536 title ||= $1 if page.blank?
530 537 end
531 538
532 539 if link_project && link_project.wiki
533 540 # extract anchor
534 541 anchor = nil
535 542 if page =~ /^(.+?)\#(.+)$/
536 543 page, anchor = $1, $2
537 544 end
538 545 # check if page exists
539 546 wiki_page = link_project.wiki.find_page(page)
540 547 url = case options[:wiki_links]
541 548 when :local; "#{title}.html"
542 549 when :anchor; "##{title}" # used for single-file wiki export
543 550 else
544 551 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
545 552 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, :id => wiki_page_id, :anchor => anchor)
546 553 end
547 554 link_to((title || page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
548 555 else
549 556 # project or wiki doesn't exist
550 557 all
551 558 end
552 559 else
553 560 all
554 561 end
555 562 end
556 563 end
557 564
558 565 # Redmine links
559 566 #
560 567 # Examples:
561 568 # Issues:
562 569 # #52 -> Link to issue #52
563 570 # Changesets:
564 571 # r52 -> Link to revision 52
565 572 # commit:a85130f -> Link to scmid starting with a85130f
566 573 # Documents:
567 574 # document#17 -> Link to document with id 17
568 575 # document:Greetings -> Link to the document with title "Greetings"
569 576 # document:"Some document" -> Link to the document with title "Some document"
570 577 # Versions:
571 578 # version#3 -> Link to version with id 3
572 579 # version:1.0.0 -> Link to version named "1.0.0"
573 580 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
574 581 # Attachments:
575 582 # attachment:file.zip -> Link to the attachment of the current object named file.zip
576 583 # Source files:
577 584 # source:some/file -> Link to the file located at /some/file in the project's repository
578 585 # source:some/file@52 -> Link to the file's revision 52
579 586 # source:some/file#L120 -> Link to line 120 of the file
580 587 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
581 588 # export:some/file -> Force the download of the file
582 589 # Forum messages:
583 590 # message#1218 -> Link to message with id 1218
584 591 def parse_redmine_links(text, project, obj, attr, only_path, options)
585 592 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(attachment|document|version|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m|
586 593 leading, esc, prefix, sep, identifier = $1, $2, $3, $5 || $7, $6 || $8
587 594 link = nil
588 595 if esc.nil?
589 596 if prefix.nil? && sep == 'r'
590 597 if project && (changeset = project.changesets.find_by_revision(identifier))
591 598 link = link_to("r#{identifier}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
592 599 :class => 'changeset',
593 600 :title => truncate_single_line(changeset.comments, :length => 100))
594 601 end
595 602 elsif sep == '#'
596 603 oid = identifier.to_i
597 604 case prefix
598 605 when nil
599 606 if issue = Issue.visible.find_by_id(oid, :include => :status)
600 607 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
601 608 :class => issue.css_classes,
602 609 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
603 610 end
604 611 when 'document'
605 612 if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
606 613 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
607 614 :class => 'document'
608 615 end
609 616 when 'version'
610 617 if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
611 618 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
612 619 :class => 'version'
613 620 end
614 621 when 'message'
615 622 if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
616 623 link = link_to h(truncate(message.subject, :length => 60)), {:only_path => only_path,
617 624 :controller => 'messages',
618 625 :action => 'show',
619 626 :board_id => message.board,
620 627 :id => message.root,
621 628 :anchor => (message.parent ? "message-#{message.id}" : nil)},
622 629 :class => 'message'
623 630 end
624 631 when 'project'
625 632 if p = Project.visible.find_by_id(oid)
626 633 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
627 634 end
628 635 end
629 636 elsif sep == ':'
630 637 # removes the double quotes if any
631 638 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
632 639 case prefix
633 640 when 'document'
634 641 if project && document = project.documents.find_by_title(name)
635 642 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
636 643 :class => 'document'
637 644 end
638 645 when 'version'
639 646 if project && version = project.versions.find_by_name(name)
640 647 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
641 648 :class => 'version'
642 649 end
643 650 when 'commit'
644 651 if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
645 652 link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
646 653 :class => 'changeset',
647 654 :title => truncate_single_line(changeset.comments, :length => 100)
648 655 end
649 656 when 'source', 'export'
650 657 if project && project.repository
651 658 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
652 659 path, rev, anchor = $1, $3, $5
653 660 link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
654 661 :path => to_path_param(path),
655 662 :rev => rev,
656 663 :anchor => anchor,
657 664 :format => (prefix == 'export' ? 'raw' : nil)},
658 665 :class => (prefix == 'export' ? 'source download' : 'source')
659 666 end
660 667 when 'attachment'
661 668 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
662 669 if attachments && attachment = attachments.detect {|a| a.filename == name }
663 670 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
664 671 :class => 'attachment'
665 672 end
666 673 when 'project'
667 674 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
668 675 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
669 676 end
670 677 end
671 678 end
672 679 end
673 680 leading + (link || "#{prefix}#{sep}#{identifier}")
674 681 end
675 682 end
676 683
677 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
678 684 HEADING_RE = /<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>/i unless const_defined?(:HEADING_RE)
679 685
680 686 # Headings and TOC
681 # Adds ids and links to headings and renders the TOC if needed unless options[:headings] is set to false
687 # Adds ids and links to headings unless options[:headings] is set to false
682 688 def parse_headings(text, project, obj, attr, only_path, options)
683 headings = []
689 return if options[:headings] == false
690
684 691 text.gsub!(HEADING_RE) do
685 692 level, attrs, content = $1.to_i, $2, $3
686 693 item = strip_tags(content).strip
687 694 anchor = item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
688 headings << [level, anchor, item]
695 @parsed_headings << [level, anchor, item]
689 696 "<h#{level} #{attrs} id=\"#{anchor}\">#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
690 end unless options[:headings] == false
691
697 end
698 end
699
700 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
701
702 # Renders the TOC with given headings
703 def replace_toc(text, headings)
692 704 text.gsub!(TOC_RE) do
693 705 if headings.empty?
694 706 ''
695 707 else
696 708 div_class = 'toc'
697 709 div_class << ' right' if $1 == '>'
698 710 div_class << ' left' if $1 == '<'
699 711 out = "<ul class=\"#{div_class}\"><li>"
700 712 root = headings.map(&:first).min
701 713 current = root
702 714 started = false
703 715 headings.each do |level, anchor, item|
704 716 if level > current
705 717 out << '<ul><li>' * (level - current)
706 718 elsif level < current
707 719 out << "</li></ul>\n" * (current - level) + "</li><li>"
708 720 elsif started
709 721 out << '</li><li>'
710 722 end
711 723 out << "<a href=\"##{anchor}\">#{item}</a>"
712 724 current = level
713 725 started = true
714 726 end
715 727 out << '</li></ul>' * (current - root)
716 728 out << '</li></ul>'
717 729 end
718 730 end
719 731 end
720 732
721 733 # Same as Rails' simple_format helper without using paragraphs
722 734 def simple_format_without_paragraph(text)
723 735 text.to_s.
724 736 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
725 737 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
726 738 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
727 739 end
728 740
729 741 def lang_options_for_select(blank=true)
730 742 (blank ? [["(auto)", ""]] : []) +
731 743 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
732 744 end
733 745
734 746 def label_tag_for(name, option_tags = nil, options = {})
735 747 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
736 748 content_tag("label", label_text)
737 749 end
738 750
739 751 def labelled_tabular_form_for(name, object, options, &proc)
740 752 options[:html] ||= {}
741 753 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
742 754 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
743 755 end
744 756
745 757 def back_url_hidden_field_tag
746 758 back_url = params[:back_url] || request.env['HTTP_REFERER']
747 759 back_url = CGI.unescape(back_url.to_s)
748 760 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
749 761 end
750 762
751 763 def check_all_links(form_name)
752 764 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
753 765 " | " +
754 766 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
755 767 end
756 768
757 769 def progress_bar(pcts, options={})
758 770 pcts = [pcts, pcts] unless pcts.is_a?(Array)
759 771 pcts = pcts.collect(&:round)
760 772 pcts[1] = pcts[1] - pcts[0]
761 773 pcts << (100 - pcts[1] - pcts[0])
762 774 width = options[:width] || '100px;'
763 775 legend = options[:legend] || ''
764 776 content_tag('table',
765 777 content_tag('tr',
766 778 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : '') +
767 779 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : '') +
768 780 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : '')
769 781 ), :class => 'progress', :style => "width: #{width};") +
770 782 content_tag('p', legend, :class => 'pourcent')
771 783 end
772 784
773 785 def checked_image(checked=true)
774 786 if checked
775 787 image_tag 'toggle_check.png'
776 788 end
777 789 end
778 790
779 791 def context_menu(url)
780 792 unless @context_menu_included
781 793 content_for :header_tags do
782 794 javascript_include_tag('context_menu') +
783 795 stylesheet_link_tag('context_menu')
784 796 end
785 797 if l(:direction) == 'rtl'
786 798 content_for :header_tags do
787 799 stylesheet_link_tag('context_menu_rtl')
788 800 end
789 801 end
790 802 @context_menu_included = true
791 803 end
792 804 javascript_tag "new ContextMenu('#{ url_for(url) }')"
793 805 end
794 806
795 807 def context_menu_link(name, url, options={})
796 808 options[:class] ||= ''
797 809 if options.delete(:selected)
798 810 options[:class] << ' icon-checked disabled'
799 811 options[:disabled] = true
800 812 end
801 813 if options.delete(:disabled)
802 814 options.delete(:method)
803 815 options.delete(:confirm)
804 816 options.delete(:onclick)
805 817 options[:class] << ' disabled'
806 818 url = '#'
807 819 end
808 820 link_to name, url, options
809 821 end
810 822
811 823 def calendar_for(field_id)
812 824 include_calendar_headers_tags
813 825 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
814 826 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
815 827 end
816 828
817 829 def include_calendar_headers_tags
818 830 unless @calendar_headers_tags_included
819 831 @calendar_headers_tags_included = true
820 832 content_for :header_tags do
821 833 start_of_week = case Setting.start_of_week.to_i
822 834 when 1
823 835 'Calendar._FD = 1;' # Monday
824 836 when 7
825 837 'Calendar._FD = 0;' # Sunday
826 838 else
827 839 '' # use language
828 840 end
829 841
830 842 javascript_include_tag('calendar/calendar') +
831 843 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
832 844 javascript_tag(start_of_week) +
833 845 javascript_include_tag('calendar/calendar-setup') +
834 846 stylesheet_link_tag('calendar')
835 847 end
836 848 end
837 849 end
838 850
839 851 def content_for(name, content = nil, &block)
840 852 @has_content ||= {}
841 853 @has_content[name] = true
842 854 super(name, content, &block)
843 855 end
844 856
845 857 def has_content?(name)
846 858 (@has_content && @has_content[name]) || false
847 859 end
848 860
849 861 # Returns the avatar image tag for the given +user+ if avatars are enabled
850 862 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
851 863 def avatar(user, options = { })
852 864 if Setting.gravatar_enabled?
853 865 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
854 866 email = nil
855 867 if user.respond_to?(:mail)
856 868 email = user.mail
857 869 elsif user.to_s =~ %r{<(.+?)>}
858 870 email = $1
859 871 end
860 872 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
861 873 else
862 874 ''
863 875 end
864 876 end
865 877
866 878 def favicon
867 879 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />"
868 880 end
869 881
870 882 # Returns true if arg is expected in the API response
871 883 def include_in_api_response?(arg)
872 884 unless @included_in_api_response
873 885 param = params[:include]
874 886 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
875 887 @included_in_api_response.collect!(&:strip)
876 888 end
877 889 @included_in_api_response.include?(arg.to_s)
878 890 end
879 891
880 892 # Returns options or nil if nometa param or X-Redmine-Nometa header
881 893 # was set in the request
882 894 def api_meta(options)
883 895 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
884 896 # compatibility mode for activeresource clients that raise
885 897 # an error when unserializing an array with attributes
886 898 nil
887 899 else
888 900 options
889 901 end
890 902 end
891 903
892 904 private
893 905
894 906 def wiki_helper
895 907 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
896 908 extend helper
897 909 return self
898 910 end
899 911
900 912 def link_to_remote_content_update(text, url_params)
901 913 link_to_remote(text,
902 914 {:url => url_params, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'},
903 915 {:href => url_for(:params => url_params)}
904 916 )
905 917 end
906 918
907 919 end
@@ -1,658 +1,662
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2010 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 ApplicationHelperTest < ActionView::TestCase
21 21
22 22 fixtures :projects, :roles, :enabled_modules, :users,
23 23 :repositories, :changesets,
24 24 :trackers, :issue_statuses, :issues, :versions, :documents,
25 25 :wikis, :wiki_pages, :wiki_contents,
26 26 :boards, :messages,
27 27 :attachments,
28 28 :enumerations
29 29
30 30 def setup
31 31 super
32 32 end
33 33
34 34 context "#link_to_if_authorized" do
35 35 context "authorized user" do
36 36 should "be tested"
37 37 end
38 38
39 39 context "unauthorized user" do
40 40 should "be tested"
41 41 end
42 42
43 43 should "allow using the :controller and :action for the target link" do
44 44 User.current = User.find_by_login('admin')
45 45
46 46 @project = Issue.first.project # Used by helper
47 47 response = link_to_if_authorized("By controller/action",
48 48 {:controller => 'issues', :action => 'edit', :id => Issue.first.id})
49 49 assert_match /href/, response
50 50 end
51 51
52 52 end
53 53
54 54 def test_auto_links
55 55 to_test = {
56 56 'http://foo.bar' => '<a class="external" href="http://foo.bar">http://foo.bar</a>',
57 57 'http://foo.bar/~user' => '<a class="external" href="http://foo.bar/~user">http://foo.bar/~user</a>',
58 58 'http://foo.bar.' => '<a class="external" href="http://foo.bar">http://foo.bar</a>.',
59 59 'https://foo.bar.' => '<a class="external" href="https://foo.bar">https://foo.bar</a>.',
60 60 'This is a link: http://foo.bar.' => 'This is a link: <a class="external" href="http://foo.bar">http://foo.bar</a>.',
61 61 'A link (eg. http://foo.bar).' => 'A link (eg. <a class="external" href="http://foo.bar">http://foo.bar</a>).',
62 62 '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>.',
63 63 'http://www.foo.bar/Test_(foobar)' => '<a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>',
64 64 '(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>)',
65 65 '(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>)',
66 66 '(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>).',
67 67 '(see "inline link":http://www.foo.bar/Test_(foobar))' => '(see <a href="http://www.foo.bar/Test_(foobar)" class="external">inline link</a>)',
68 68 '(see "inline link":http://www.foo.bar/Test)' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>)',
69 69 '(see "inline link":http://www.foo.bar/Test).' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>).',
70 70 'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>',
71 71 '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>',
72 72 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>',
73 73 'http://foo@www.bar.com' => '<a class="external" href="http://foo@www.bar.com">http://foo@www.bar.com</a>',
74 74 'http://foo:bar@www.bar.com' => '<a class="external" href="http://foo:bar@www.bar.com">http://foo:bar@www.bar.com</a>',
75 75 'ftp://foo.bar' => '<a class="external" href="ftp://foo.bar">ftp://foo.bar</a>',
76 76 'ftps://foo.bar' => '<a class="external" href="ftps://foo.bar">ftps://foo.bar</a>',
77 77 'sftp://foo.bar' => '<a class="external" href="sftp://foo.bar">sftp://foo.bar</a>',
78 78 # two exclamation marks
79 79 '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>',
80 80 # escaping
81 81 'http://foo"bar' => '<a class="external" href="http://foo&quot;bar">http://foo"bar</a>',
82 82 # wrap in angle brackets
83 83 '<http://foo.bar>' => '&lt;<a class="external" href="http://foo.bar">http://foo.bar</a>&gt;'
84 84 }
85 85 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
86 86 end
87 87
88 88 def test_auto_mailto
89 89 assert_equal '<p><a class="email" href="mailto:test@foo.bar">test@foo.bar</a></p>',
90 90 textilizable('test@foo.bar')
91 91 end
92 92
93 93 def test_inline_images
94 94 to_test = {
95 95 '!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
96 96 'floating !>http://foo.bar/image.jpg!' => 'floating <div style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></div>',
97 97 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class <img src="http://foo.bar/image.jpg" class="some-class" alt="" />',
98 98 # inline styles should be stripped
99 99 'with style !{width:100px;height100px}http://foo.bar/image.jpg!' => 'with style <img src="http://foo.bar/image.jpg" alt="" />',
100 100 '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" />',
101 101 '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;" />',
102 102 }
103 103 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
104 104 end
105 105
106 106 def test_inline_images_inside_tags
107 107 raw = <<-RAW
108 108 h1. !foo.png! Heading
109 109
110 110 Centered image:
111 111
112 112 p=. !bar.gif!
113 113 RAW
114 114
115 115 assert textilizable(raw).include?('<img src="foo.png" alt="" />')
116 116 assert textilizable(raw).include?('<img src="bar.gif" alt="" />')
117 117 end
118 118
119 119 def test_attached_images
120 120 to_test = {
121 121 'Inline image: !logo.gif!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
122 122 'Inline image: !logo.GIF!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
123 123 'No match: !ogo.gif!' => 'No match: <img src="ogo.gif" alt="" />',
124 124 'No match: !ogo.GIF!' => 'No match: <img src="ogo.GIF" alt="" />',
125 125 # link image
126 126 '!logo.gif!:http://foo.bar/' => '<a href="http://foo.bar/"><img src="/attachments/download/3" title="This is a logo" alt="This is a logo" /></a>',
127 127 }
128 128 attachments = Attachment.find(:all)
129 129 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
130 130 end
131 131
132 132 def test_textile_external_links
133 133 to_test = {
134 134 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>',
135 135 'This is an intern "link":/foo/bar' => 'This is an intern <a href="/foo/bar">link</a>',
136 136 '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>',
137 137 '"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>',
138 138 "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":</p>\n\n\n\t<p>Another paragraph",
139 139 # no multiline link text
140 140 "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",
141 141 # mailto link
142 142 "\"system administrator\":mailto:sysadmin@example.com?subject=redmine%20permissions" => "<a href=\"mailto:sysadmin@example.com?subject=redmine%20permissions\">system administrator</a>",
143 143 # two exclamation marks
144 144 '"a link":http://example.net/path!602815048C7B5C20!302.html' => '<a href="http://example.net/path!602815048C7B5C20!302.html" class="external">a link</a>',
145 145 # escaping
146 146 '"test":http://foo"bar' => '<a href="http://foo&quot;bar" class="external">test</a>',
147 147 }
148 148 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
149 149 end
150 150
151 151 def test_redmine_links
152 152 issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3},
153 153 :class => 'issue status-1 priority-1 overdue', :title => 'Error 281 when updating a recipe (New)')
154 154
155 155 changeset_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
156 156 :class => 'changeset', :title => 'My very first commit')
157 157 changeset_link2 = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
158 158 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
159 159
160 160 document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
161 161 :class => 'document')
162 162
163 163 version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
164 164 :class => 'version')
165 165
166 166 message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4}
167 167
168 168 project_url = {:controller => 'projects', :action => 'show', :id => 'subproject1'}
169 169
170 170 source_url = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}
171 171 source_url_with_ext = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file.ext']}
172 172
173 173 to_test = {
174 174 # tickets
175 175 '#3, [#3], (#3) and #3.' => "#{issue_link}, [#{issue_link}], (#{issue_link}) and #{issue_link}.",
176 176 # changesets
177 177 'r1' => changeset_link,
178 178 'r1.' => "#{changeset_link}.",
179 179 'r1, r2' => "#{changeset_link}, #{changeset_link2}",
180 180 'r1,r2' => "#{changeset_link},#{changeset_link2}",
181 181 # documents
182 182 'document#1' => document_link,
183 183 'document:"Test document"' => document_link,
184 184 # versions
185 185 'version#2' => version_link,
186 186 'version:1.0' => version_link,
187 187 'version:"1.0"' => version_link,
188 188 # source
189 189 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
190 190 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".",
191 191 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
192 192 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".",
193 193 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
194 194 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",",
195 195 'source:/some/file@52' => link_to('source:/some/file@52', source_url.merge(:rev => 52), :class => 'source'),
196 196 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_ext.merge(:rev => 52), :class => 'source'),
197 197 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url.merge(:anchor => 'L110'), :class => 'source'),
198 198 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext.merge(:anchor => 'L110'), :class => 'source'),
199 199 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url.merge(:rev => 52, :anchor => 'L110'), :class => 'source'),
200 200 'export:/some/file' => link_to('export:/some/file', source_url.merge(:format => 'raw'), :class => 'source download'),
201 201 # message
202 202 'message#4' => link_to('Post 2', message_url, :class => 'message'),
203 203 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5'), :class => 'message'),
204 204 # project
205 205 'project#3' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
206 206 'project:subproject1' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
207 207 'project:"eCookbook subProject 1"' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
208 208 # escaping
209 209 '!#3.' => '#3.',
210 210 '!r1' => 'r1',
211 211 '!document#1' => 'document#1',
212 212 '!document:"Test document"' => 'document:"Test document"',
213 213 '!version#2' => 'version#2',
214 214 '!version:1.0' => 'version:1.0',
215 215 '!version:"1.0"' => 'version:"1.0"',
216 216 '!source:/some/file' => 'source:/some/file',
217 217 # not found
218 218 '#0123456789' => '#0123456789',
219 219 # invalid expressions
220 220 'source:' => 'source:',
221 221 # url hash
222 222 "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
223 223 }
224 224 @project = Project.find(1)
225 225 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
226 226 end
227 227
228 228 def test_attachment_links
229 229 attachment_link = link_to('error281.txt', {:controller => 'attachments', :action => 'download', :id => '1'}, :class => 'attachment')
230 230 to_test = {
231 231 'attachment:error281.txt' => attachment_link
232 232 }
233 233 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => Issue.find(3).attachments), "#{text} failed" }
234 234 end
235 235
236 236 def test_wiki_links
237 237 to_test = {
238 238 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
239 239 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
240 240 # link with anchor
241 241 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
242 242 '[[Another page#anchor|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page#anchor" class="wiki-page">Page</a>',
243 243 # page that doesn't exist
244 244 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
245 245 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">404</a>',
246 246 # link to another project wiki
247 247 '[[onlinestore:]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">onlinestore</a>',
248 248 '[[onlinestore:|Wiki]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">Wiki</a>',
249 249 '[[onlinestore:Start page]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Start page</a>',
250 250 '[[onlinestore:Start page|Text]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Text</a>',
251 251 '[[onlinestore:Unknown page]]' => '<a href="/projects/onlinestore/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
252 252 # striked through link
253 253 '-[[Another page|Page]]-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a></del>',
254 254 '-[[Another page|Page]] link-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a> link</del>',
255 255 # escaping
256 256 '![[Another page|Page]]' => '[[Another page|Page]]',
257 257 # project does not exist
258 258 '[[unknowproject:Start]]' => '[[unknowproject:Start]]',
259 259 '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]',
260 260 }
261 261 @project = Project.find(1)
262 262 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
263 263 end
264 264
265 265 def test_html_tags
266 266 to_test = {
267 267 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
268 268 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
269 269 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
270 270 # do not escape pre/code tags
271 271 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
272 272 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
273 273 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
274 274 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
275 275 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
276 276 # remove attributes except class
277 277 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
278 278 '<pre class="foo">some text</pre>' => '<pre class="foo">some text</pre>',
279 279 "<pre class='foo bar'>some text</pre>" => "<pre class='foo bar'>some text</pre>",
280 280 '<pre class="foo bar">some text</pre>' => '<pre class="foo bar">some text</pre>',
281 281 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
282 282 # xss
283 283 '<pre><code class=""onmouseover="alert(1)">text</code></pre>' => '<pre><code>text</code></pre>',
284 284 '<pre class=""onmouseover="alert(1)">text</pre>' => '<pre>text</pre>',
285 285 }
286 286 to_test.each { |text, result| assert_equal result, textilizable(text) }
287 287 end
288 288
289 289 def test_allowed_html_tags
290 290 to_test = {
291 291 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
292 292 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
293 293 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
294 294 }
295 295 to_test.each { |text, result| assert_equal result, textilizable(text) }
296 296 end
297 297
298 298 def test_pre_tags
299 299 raw = <<-RAW
300 300 Before
301 301
302 302 <pre>
303 303 <prepared-statement-cache-size>32</prepared-statement-cache-size>
304 304 </pre>
305 305
306 306 After
307 307 RAW
308 308
309 309 expected = <<-EXPECTED
310 310 <p>Before</p>
311 311 <pre>
312 312 &lt;prepared-statement-cache-size&gt;32&lt;/prepared-statement-cache-size&gt;
313 313 </pre>
314 314 <p>After</p>
315 315 EXPECTED
316 316
317 317 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
318 318 end
319 319
320 320 def test_pre_content_should_not_parse_wiki_and_redmine_links
321 321 raw = <<-RAW
322 322 [[CookBook documentation]]
323 323
324 324 #1
325 325
326 326 <pre>
327 327 [[CookBook documentation]]
328 328
329 329 #1
330 330 </pre>
331 331 RAW
332 332
333 333 expected = <<-EXPECTED
334 334 <p><a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a></p>
335 335 <p><a href="/issues/1" class="issue status-1 priority-1" title="Can't print recipes (New)">#1</a></p>
336 336 <pre>
337 337 [[CookBook documentation]]
338 338
339 339 #1
340 340 </pre>
341 341 EXPECTED
342 342
343 343 @project = Project.find(1)
344 344 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
345 345 end
346 346
347 347 def test_non_closing_pre_blocks_should_be_closed
348 348 raw = <<-RAW
349 349 <pre><code>
350 350 RAW
351 351
352 352 expected = <<-EXPECTED
353 353 <pre><code>
354 354 </code></pre>
355 355 EXPECTED
356 356
357 357 @project = Project.find(1)
358 358 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
359 359 end
360 360
361 361 def test_syntax_highlight
362 362 raw = <<-RAW
363 363 <pre><code class="ruby">
364 364 # Some ruby code here
365 365 </code></pre>
366 366 RAW
367 367
368 368 expected = <<-EXPECTED
369 369 <pre><code class="ruby syntaxhl"><span class=\"CodeRay\"><span class="no">1</span> <span class="c"># Some ruby code here</span></span>
370 370 </code></pre>
371 371 EXPECTED
372 372
373 373 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
374 374 end
375 375
376 376 def test_wiki_links_in_tables
377 377 to_test = {"|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" =>
378 378 '<tr><td><a href="/projects/ecookbook/wiki/Page" class="wiki-page new">Link title</a></td>' +
379 379 '<td><a href="/projects/ecookbook/wiki/Other_Page" class="wiki-page new">Other title</a></td>' +
380 380 '</tr><tr><td>Cell 21</td><td><a href="/projects/ecookbook/wiki/Last_page" class="wiki-page new">Last page</a></td></tr>'
381 381 }
382 382 @project = Project.find(1)
383 383 to_test.each { |text, result| assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '') }
384 384 end
385 385
386 386 def test_text_formatting
387 387 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
388 388 '(_text within parentheses_)' => '(<em>text within parentheses</em>)',
389 389 'a *Humane Web* Text Generator' => 'a <strong>Humane Web</strong> Text Generator',
390 390 '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>',
391 391 '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',
392 392 }
393 393 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
394 394 end
395 395
396 396 def test_wiki_horizontal_rule
397 397 assert_equal '<hr />', textilizable('---')
398 398 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
399 399 end
400 400
401 401 def test_footnotes
402 402 raw = <<-RAW
403 403 This is some text[1].
404 404
405 405 fn1. This is the foot note
406 406 RAW
407 407
408 408 expected = <<-EXPECTED
409 409 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
410 410 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
411 411 EXPECTED
412 412
413 413 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
414 414 end
415 415
416 416 def test_table_of_content
417 417 raw = <<-RAW
418 418 {{toc}}
419 419
420 420 h1. Title
421 421
422 422 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
423 423
424 424 h2. Subtitle with a [[Wiki]] link
425 425
426 426 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
427 427
428 428 h2. Subtitle with [[Wiki|another Wiki]] link
429 429
430 430 h2. Subtitle with %{color:red}red text%
431
431
432 <pre>
433 some code
434 </pre>
435
432 436 h3. Subtitle with *some* _modifiers_
433 437
434 438 h1. Another title
435 439
436 440 h3. An "Internet link":http://www.redmine.org/ inside subtitle
437 441
438 442 h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
439 443
440 444 RAW
441 445
442 446 expected = '<ul class="toc">' +
443 447 '<li><a href="#Title">Title</a>' +
444 448 '<ul>' +
445 449 '<li><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
446 450 '<li><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
447 451 '<li><a href="#Subtitle-with-red-text">Subtitle with red text</a>' +
448 452 '<ul>' +
449 453 '<li><a href="#Subtitle-with-some-modifiers">Subtitle with some modifiers</a></li>' +
450 454 '</ul>' +
451 455 '</li>' +
452 456 '</ul>' +
453 457 '</li>' +
454 458 '<li><a href="#Another-title">Another title</a>' +
455 459 '<ul>' +
456 460 '<li>' +
457 461 '<ul>' +
458 462 '<li><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
459 463 '</ul>' +
460 464 '</li>' +
461 465 '<li><a href="#Project-Name">Project Name</a></li>' +
462 466 '</ul>' +
463 467 '</li>' +
464 468 '</ul>'
465 469
466 470 @project = Project.find(1)
467 assert textilizable(raw).gsub("\n", "").include?(expected)
471 assert textilizable(raw).gsub("\n", "").include?(expected), textilizable(raw)
468 472 end
469 473
470 474 def test_table_of_content_should_contain_included_page_headings
471 475 raw = <<-RAW
472 476 {{toc}}
473 477
474 478 h1. Included
475 479
476 480 {{include(Child_1)}}
477 481 RAW
478 482
479 483 expected = '<ul class="toc">' +
480 484 '<li><a href="#Included">Included</a></li>' +
481 485 '<li><a href="#Child-page-1">Child page 1</a></li>' +
482 486 '</ul>'
483 487
484 488 @project = Project.find(1)
485 489 assert textilizable(raw).gsub("\n", "").include?(expected)
486 490 end
487 491
488 492 def test_blockquote
489 493 # orig raw text
490 494 raw = <<-RAW
491 495 John said:
492 496 > Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
493 497 > Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
494 498 > * Donec odio lorem,
495 499 > * sagittis ac,
496 500 > * malesuada in,
497 501 > * adipiscing eu, dolor.
498 502 >
499 503 > >Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.
500 504 > Proin a tellus. Nam vel neque.
501 505
502 506 He's right.
503 507 RAW
504 508
505 509 # expected html
506 510 expected = <<-EXPECTED
507 511 <p>John said:</p>
508 512 <blockquote>
509 513 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
510 514 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
511 515 <ul>
512 516 <li>Donec odio lorem,</li>
513 517 <li>sagittis ac,</li>
514 518 <li>malesuada in,</li>
515 519 <li>adipiscing eu, dolor.</li>
516 520 </ul>
517 521 <blockquote>
518 522 <p>Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.</p>
519 523 </blockquote>
520 524 <p>Proin a tellus. Nam vel neque.</p>
521 525 </blockquote>
522 526 <p>He's right.</p>
523 527 EXPECTED
524 528
525 529 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
526 530 end
527 531
528 532 def test_table
529 533 raw = <<-RAW
530 534 This is a table with empty cells:
531 535
532 536 |cell11|cell12||
533 537 |cell21||cell23|
534 538 |cell31|cell32|cell33|
535 539 RAW
536 540
537 541 expected = <<-EXPECTED
538 542 <p>This is a table with empty cells:</p>
539 543
540 544 <table>
541 545 <tr><td>cell11</td><td>cell12</td><td></td></tr>
542 546 <tr><td>cell21</td><td></td><td>cell23</td></tr>
543 547 <tr><td>cell31</td><td>cell32</td><td>cell33</td></tr>
544 548 </table>
545 549 EXPECTED
546 550
547 551 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
548 552 end
549 553
550 554 def test_table_with_line_breaks
551 555 raw = <<-RAW
552 556 This is a table with line breaks:
553 557
554 558 |cell11
555 559 continued|cell12||
556 560 |-cell21-||cell23
557 561 cell23 line2
558 562 cell23 *line3*|
559 563 |cell31|cell32
560 564 cell32 line2|cell33|
561 565
562 566 RAW
563 567
564 568 expected = <<-EXPECTED
565 569 <p>This is a table with line breaks:</p>
566 570
567 571 <table>
568 572 <tr>
569 573 <td>cell11<br />continued</td>
570 574 <td>cell12</td>
571 575 <td></td>
572 576 </tr>
573 577 <tr>
574 578 <td><del>cell21</del></td>
575 579 <td></td>
576 580 <td>cell23<br/>cell23 line2<br/>cell23 <strong>line3</strong></td>
577 581 </tr>
578 582 <tr>
579 583 <td>cell31</td>
580 584 <td>cell32<br/>cell32 line2</td>
581 585 <td>cell33</td>
582 586 </tr>
583 587 </table>
584 588 EXPECTED
585 589
586 590 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
587 591 end
588 592
589 593 def test_textile_should_not_mangle_brackets
590 594 assert_equal '<p>[msg1][msg2]</p>', textilizable('[msg1][msg2]')
591 595 end
592 596
593 597 def test_default_formatter
594 598 Setting.text_formatting = 'unknown'
595 599 text = 'a *link*: http://www.example.net/'
596 600 assert_equal '<p>a *link*: <a href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
597 601 Setting.text_formatting = 'textile'
598 602 end
599 603
600 604 def test_due_date_distance_in_words
601 605 to_test = { Date.today => 'Due in 0 days',
602 606 Date.today + 1 => 'Due in 1 day',
603 607 Date.today + 100 => 'Due in about 3 months',
604 608 Date.today + 20000 => 'Due in over 54 years',
605 609 Date.today - 1 => '1 day late',
606 610 Date.today - 100 => 'about 3 months late',
607 611 Date.today - 20000 => 'over 54 years late',
608 612 }
609 613 to_test.each do |date, expected|
610 614 assert_equal expected, due_date_distance_in_words(date)
611 615 end
612 616 end
613 617
614 618 def test_avatar
615 619 # turn on avatars
616 620 Setting.gravatar_enabled = '1'
617 621 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
618 622 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
619 623 assert_nil avatar('jsmith')
620 624 assert_nil avatar(nil)
621 625
622 626 # turn off avatars
623 627 Setting.gravatar_enabled = '0'
624 628 assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
625 629 end
626 630
627 631 def test_link_to_user
628 632 user = User.find(2)
629 633 t = link_to_user(user)
630 634 assert_equal "<a href=\"/users/2\">#{ user.name }</a>", t
631 635 end
632 636
633 637 def test_link_to_user_should_not_link_to_locked_user
634 638 user = User.find(5)
635 639 assert user.locked?
636 640 t = link_to_user(user)
637 641 assert_equal user.name, t
638 642 end
639 643
640 644 def test_link_to_user_should_not_link_to_anonymous
641 645 user = User.anonymous
642 646 assert user.anonymous?
643 647 t = link_to_user(user)
644 648 assert_equal ::I18n.t(:label_user_anonymous), t
645 649 end
646 650
647 651 def test_link_to_project
648 652 project = Project.find(1)
649 653 assert_equal %(<a href="/projects/ecookbook">eCookbook</a>),
650 654 link_to_project(project)
651 655 assert_equal %(<a href="/projects/ecookbook/settings">eCookbook</a>),
652 656 link_to_project(project, :action => 'settings')
653 657 assert_equal %(<a href="http://test.host/projects/ecookbook?jump=blah">eCookbook</a>),
654 658 link_to_project(project, {:only_path => false, :jump => 'blah'})
655 659 assert_equal %(<a href="/projects/ecookbook/settings" class="project">eCookbook</a>),
656 660 link_to_project(project, {:action => 'settings'}, :class => "project")
657 661 end
658 662 end
General Comments 0
You need to be logged in to leave comments. Login now