##// END OF EJS Templates
Merged r7558 from trunk (#3276)....
Etienne Massip -
r7439:4682acf0402e
parent child
Show More
@@ -1,942 +1,946
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 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 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
108 108
109 109 link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => rev},
110 110 :title => l(:label_revision_id, format_revision(revision)))
111 111 end
112 112
113 113 # Generates a link to a message
114 114 def link_to_message(message, options={}, html_options = nil)
115 115 link_to(
116 116 h(truncate(message.subject, :length => 60)),
117 117 { :controller => 'messages', :action => 'show',
118 118 :board_id => message.board_id,
119 119 :id => message.root,
120 120 :r => (message.parent_id && message.id),
121 121 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
122 122 }.merge(options),
123 123 html_options
124 124 )
125 125 end
126 126
127 127 # Generates a link to a project if active
128 128 # Examples:
129 129 #
130 130 # link_to_project(project) # => link to the specified project overview
131 131 # link_to_project(project, :action=>'settings') # => link to project settings
132 132 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
133 133 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
134 134 #
135 135 def link_to_project(project, options={}, html_options = nil)
136 136 if project.active?
137 137 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
138 138 link_to(h(project), url, html_options)
139 139 else
140 140 h(project)
141 141 end
142 142 end
143 143
144 144 def toggle_link(name, id, options={})
145 145 onclick = "Element.toggle('#{id}'); "
146 146 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
147 147 onclick << "return false;"
148 148 link_to(name, "#", :onclick => onclick)
149 149 end
150 150
151 151 def image_to_function(name, function, html_options = {})
152 152 html_options.symbolize_keys!
153 153 tag(:input, html_options.merge({
154 154 :type => "image", :src => image_path(name),
155 155 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
156 156 }))
157 157 end
158 158
159 159 def prompt_to_remote(name, text, param, url, html_options = {})
160 160 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
161 161 link_to name, {}, html_options
162 162 end
163 163
164 164 def format_activity_title(text)
165 165 h(truncate_single_line(text, :length => 100))
166 166 end
167 167
168 168 def format_activity_day(date)
169 169 date == Date.today ? l(:label_today).titleize : format_date(date)
170 170 end
171 171
172 172 def format_activity_description(text)
173 173 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
174 174 end
175 175
176 176 def format_version_name(version)
177 177 if version.project == @project
178 178 h(version)
179 179 else
180 180 h("#{version.project} - #{version}")
181 181 end
182 182 end
183 183
184 184 def due_date_distance_in_words(date)
185 185 if date
186 186 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
187 187 end
188 188 end
189 189
190 190 def render_page_hierarchy(pages, node=nil, options={})
191 191 content = ''
192 192 if pages[node]
193 193 content << "<ul class=\"pages-hierarchy\">\n"
194 194 pages[node].each do |page|
195 195 content << "<li>"
196 196 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
197 197 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
198 198 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
199 199 content << "</li>\n"
200 200 end
201 201 content << "</ul>\n"
202 202 end
203 203 content
204 204 end
205 205
206 206 # Renders flash messages
207 207 def render_flash_messages
208 208 s = ''
209 209 flash.each do |k,v|
210 210 s << content_tag('div', v, :class => "flash #{k}")
211 211 end
212 212 s
213 213 end
214 214
215 215 # Renders tabs and their content
216 216 def render_tabs(tabs)
217 217 if tabs.any?
218 218 render :partial => 'common/tabs', :locals => {:tabs => tabs}
219 219 else
220 220 content_tag 'p', l(:label_no_data), :class => "nodata"
221 221 end
222 222 end
223 223
224 224 # Renders the project quick-jump box
225 225 def render_project_jump_box
226 226 return unless User.current.logged?
227 227 projects = User.current.memberships.collect(&:project).compact.uniq
228 228 if projects.any?
229 229 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
230 230 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
231 231 '<option value="" disabled="disabled">---</option>'
232 232 s << project_tree_options_for_select(projects, :selected => @project) do |p|
233 233 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
234 234 end
235 235 s << '</select>'
236 236 s
237 237 end
238 238 end
239 239
240 240 def project_tree_options_for_select(projects, options = {})
241 241 s = ''
242 242 project_tree(projects) do |project, level|
243 243 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
244 244 tag_options = {:value => project.id}
245 245 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
246 246 tag_options[:selected] = 'selected'
247 247 else
248 248 tag_options[:selected] = nil
249 249 end
250 250 tag_options.merge!(yield(project)) if block_given?
251 251 s << content_tag('option', name_prefix + h(project), tag_options)
252 252 end
253 253 s
254 254 end
255 255
256 256 # Yields the given block for each project with its level in the tree
257 257 #
258 258 # Wrapper for Project#project_tree
259 259 def project_tree(projects, &block)
260 260 Project.project_tree(projects, &block)
261 261 end
262 262
263 263 def project_nested_ul(projects, &block)
264 264 s = ''
265 265 if projects.any?
266 266 ancestors = []
267 267 projects.sort_by(&:lft).each do |project|
268 268 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
269 269 s << "<ul>\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 s << "<li>"
279 279 s << yield(project).to_s
280 280 ancestors << project
281 281 end
282 282 s << ("</li></ul>\n" * ancestors.size)
283 283 end
284 284 s
285 285 end
286 286
287 287 def principals_check_box_tags(name, principals)
288 288 s = ''
289 289 principals.sort.each do |principal|
290 290 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
291 291 end
292 292 s
293 293 end
294 294
295 295 # Truncates and returns the string as a single line
296 296 def truncate_single_line(string, *args)
297 297 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
298 298 end
299 299
300 300 # Truncates at line break after 250 characters or options[:length]
301 301 def truncate_lines(string, options={})
302 302 length = options[:length] || 250
303 303 if string.to_s =~ /\A(.{#{length}}.*?)$/m
304 304 "#{$1}..."
305 305 else
306 306 string
307 307 end
308 308 end
309 309
310 310 def html_hours(text)
311 311 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
312 312 end
313 313
314 314 def authoring(created, author, options={})
315 315 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created))
316 316 end
317 317
318 318 def time_tag(time)
319 319 text = distance_of_time_in_words(Time.now, time)
320 320 if @project
321 321 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
322 322 else
323 323 content_tag('acronym', text, :title => format_time(time))
324 324 end
325 325 end
326 326
327 327 def syntax_highlight(name, content)
328 328 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
329 329 end
330 330
331 331 def to_path_param(path)
332 332 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
333 333 end
334 334
335 335 def pagination_links_full(paginator, count=nil, options={})
336 336 page_param = options.delete(:page_param) || :page
337 337 per_page_links = options.delete(:per_page_links)
338 338 url_param = params.dup
339 339
340 340 html = ''
341 341 if paginator.current.previous
342 342 html << link_to_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
343 343 end
344 344
345 345 html << (pagination_links_each(paginator, options) do |n|
346 346 link_to_content_update(n.to_s, url_param.merge(page_param => n))
347 347 end || '')
348 348
349 349 if paginator.current.next
350 350 html << ' ' + link_to_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next))
351 351 end
352 352
353 353 unless count.nil?
354 354 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
355 355 if per_page_links != false && links = per_page_links(paginator.items_per_page)
356 356 html << " | #{links}"
357 357 end
358 358 end
359 359
360 360 html
361 361 end
362 362
363 363 def per_page_links(selected=nil)
364 364 links = Setting.per_page_options_array.collect do |n|
365 365 n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
366 366 end
367 367 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
368 368 end
369 369
370 370 def reorder_links(name, url)
371 371 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), url.merge({"#{name}[move_to]" => 'highest'}), :method => :post, :title => l(:label_sort_highest)) +
372 372 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), url.merge({"#{name}[move_to]" => 'higher'}), :method => :post, :title => l(:label_sort_higher)) +
373 373 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), url.merge({"#{name}[move_to]" => 'lower'}), :method => :post, :title => l(:label_sort_lower)) +
374 374 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), url.merge({"#{name}[move_to]" => 'lowest'}), :method => :post, :title => l(:label_sort_lowest))
375 375 end
376 376
377 377 def breadcrumb(*args)
378 378 elements = args.flatten
379 379 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
380 380 end
381 381
382 382 def other_formats_links(&block)
383 383 concat('<p class="other-formats">' + l(:label_export_to))
384 384 yield Redmine::Views::OtherFormatsBuilder.new(self)
385 385 concat('</p>')
386 386 end
387 387
388 388 def page_header_title
389 389 if @project.nil? || @project.new_record?
390 390 h(Setting.app_title)
391 391 else
392 392 b = []
393 393 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
394 394 if ancestors.any?
395 395 root = ancestors.shift
396 396 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
397 397 if ancestors.size > 2
398 398 b << '&#8230;'
399 399 ancestors = ancestors[-2, 2]
400 400 end
401 401 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
402 402 end
403 403 b << h(@project)
404 404 b.join(' &#187; ')
405 405 end
406 406 end
407 407
408 408 def html_title(*args)
409 409 if args.empty?
410 410 title = []
411 411 title << @project.name if @project
412 412 title += @html_title if @html_title
413 413 title << Setting.app_title
414 414 title.select {|t| !t.blank? }.join(' - ')
415 415 else
416 416 @html_title ||= []
417 417 @html_title += args
418 418 end
419 419 end
420 420
421 421 # Returns the theme, controller name, and action as css classes for the
422 422 # HTML body.
423 423 def body_css_classes
424 424 css = []
425 425 if theme = Redmine::Themes.theme(Setting.ui_theme)
426 426 css << 'theme-' + theme.name
427 427 end
428 428
429 429 css << 'controller-' + params[:controller]
430 430 css << 'action-' + params[:action]
431 431 css.join(' ')
432 432 end
433 433
434 434 def accesskey(s)
435 435 Redmine::AccessKeys.key_for s
436 436 end
437 437
438 438 # Formats text according to system settings.
439 439 # 2 ways to call this method:
440 440 # * with a String: textilizable(text, options)
441 441 # * with an object and one of its attribute: textilizable(issue, :description, options)
442 442 def textilizable(*args)
443 443 options = args.last.is_a?(Hash) ? args.pop : {}
444 444 case args.size
445 445 when 1
446 446 obj = options[:object]
447 447 text = args.shift
448 448 when 2
449 449 obj = args.shift
450 450 attr = args.shift
451 451 text = obj.send(attr).to_s
452 452 else
453 453 raise ArgumentError, 'invalid arguments to textilizable'
454 454 end
455 455 return '' if text.blank?
456 456 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
457 457 only_path = options.delete(:only_path) == false ? false : true
458 458
459 459 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }
460 460
461 461 @parsed_headings = []
462 462 text = parse_non_pre_blocks(text) do |text|
463 463 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_headings].each do |method_name|
464 464 send method_name, text, project, obj, attr, only_path, options
465 465 end
466 466 end
467 467
468 468 if @parsed_headings.any?
469 469 replace_toc(text, @parsed_headings)
470 470 end
471 471
472 472 text
473 473 end
474 474
475 475 def parse_non_pre_blocks(text)
476 476 s = StringScanner.new(text)
477 477 tags = []
478 478 parsed = ''
479 479 while !s.eos?
480 480 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
481 481 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
482 482 if tags.empty?
483 483 yield text
484 484 end
485 485 parsed << text
486 486 if tag
487 487 if closing
488 488 if tags.last == tag.downcase
489 489 tags.pop
490 490 end
491 491 else
492 492 tags << tag.downcase
493 493 end
494 494 parsed << full_tag
495 495 end
496 496 end
497 497 # Close any non closing tags
498 498 while tag = tags.pop
499 499 parsed << "</#{tag}>"
500 500 end
501 501 parsed
502 502 end
503 503
504 504 def parse_inline_attachments(text, project, obj, attr, only_path, options)
505 505 # when using an image link, try to use an attachment, if possible
506 506 if options[:attachments] || (obj && obj.respond_to?(:attachments))
507 507 attachments = nil
508 508 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
509 509 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
510 510 attachments ||= (options[:attachments] || obj.attachments).sort_by(&:created_on).reverse
511 511 # search for the picture in attachments
512 512 if found = attachments.detect { |att| att.filename.downcase == filename }
513 513 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
514 514 desc = found.description.to_s.gsub('"', '')
515 515 if !desc.blank? && alttext.blank?
516 516 alt = " title=\"#{desc}\" alt=\"#{desc}\""
517 517 end
518 518 "src=\"#{image_url}\"#{alt}"
519 519 else
520 520 m
521 521 end
522 522 end
523 523 end
524 524 end
525 525
526 526 # Wiki links
527 527 #
528 528 # Examples:
529 529 # [[mypage]]
530 530 # [[mypage|mytext]]
531 531 # wiki links can refer other project wikis, using project name or identifier:
532 532 # [[project:]] -> wiki starting page
533 533 # [[project:|mytext]]
534 534 # [[project:mypage]]
535 535 # [[project:mypage|mytext]]
536 536 def parse_wiki_links(text, project, obj, attr, only_path, options)
537 537 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
538 538 link_project = project
539 539 esc, all, page, title = $1, $2, $3, $5
540 540 if esc.nil?
541 541 if page =~ /^([^\:]+)\:(.*)$/
542 542 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
543 543 page = $2
544 544 title ||= $1 if page.blank?
545 545 end
546 546
547 547 if link_project && link_project.wiki
548 548 # extract anchor
549 549 anchor = nil
550 550 if page =~ /^(.+?)\#(.+)$/
551 551 page, anchor = $1, $2
552 552 end
553 553 # check if page exists
554 554 wiki_page = link_project.wiki.find_page(page)
555 url = case options[:wiki_links]
555 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
556 "##{anchor}"
557 else
558 case options[:wiki_links]
556 559 when :local; "#{title}.html"
557 560 when :anchor; "##{title}" # used for single-file wiki export
558 561 else
559 562 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
560 563 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, :id => wiki_page_id, :anchor => anchor)
561 564 end
565 end
562 566 link_to((title || page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
563 567 else
564 568 # project or wiki doesn't exist
565 569 all
566 570 end
567 571 else
568 572 all
569 573 end
570 574 end
571 575 end
572 576
573 577 # Redmine links
574 578 #
575 579 # Examples:
576 580 # Issues:
577 581 # #52 -> Link to issue #52
578 582 # Changesets:
579 583 # r52 -> Link to revision 52
580 584 # commit:a85130f -> Link to scmid starting with a85130f
581 585 # Documents:
582 586 # document#17 -> Link to document with id 17
583 587 # document:Greetings -> Link to the document with title "Greetings"
584 588 # document:"Some document" -> Link to the document with title "Some document"
585 589 # Versions:
586 590 # version#3 -> Link to version with id 3
587 591 # version:1.0.0 -> Link to version named "1.0.0"
588 592 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
589 593 # Attachments:
590 594 # attachment:file.zip -> Link to the attachment of the current object named file.zip
591 595 # Source files:
592 596 # source:some/file -> Link to the file located at /some/file in the project's repository
593 597 # source:some/file@52 -> Link to the file's revision 52
594 598 # source:some/file#L120 -> Link to line 120 of the file
595 599 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
596 600 # export:some/file -> Force the download of the file
597 601 # Forum messages:
598 602 # message#1218 -> Link to message with id 1218
599 603 #
600 604 # Links can refer other objects from other projects, using project identifier:
601 605 # identifier:r52
602 606 # identifier:document:"Some document"
603 607 # identifier:version:1.0.0
604 608 # identifier:source:some/file
605 609 def parse_redmine_links(text, project, obj, attr, only_path, options)
606 610 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-]+):)?(attachment|document|version|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m|
607 611 leading, esc, project_prefix, project_identifier, prefix, sep, identifier = $1, $2, $3, $4, $5, $7 || $9, $8 || $10
608 612 link = nil
609 613 if project_identifier
610 614 project = Project.visible.find_by_identifier(project_identifier)
611 615 end
612 616 if esc.nil?
613 617 if prefix.nil? && sep == 'r'
614 618 # project.changesets.visible raises an SQL error because of a double join on repositories
615 619 if project && project.repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(project.repository.id, identifier))
616 620 link = link_to("#{project_prefix}r#{identifier}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
617 621 :class => 'changeset',
618 622 :title => truncate_single_line(changeset.comments, :length => 100))
619 623 end
620 624 elsif sep == '#'
621 625 oid = identifier.to_i
622 626 case prefix
623 627 when nil
624 628 if issue = Issue.visible.find_by_id(oid, :include => :status)
625 629 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
626 630 :class => issue.css_classes,
627 631 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
628 632 end
629 633 when 'document'
630 634 if document = Document.visible.find_by_id(oid)
631 635 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
632 636 :class => 'document'
633 637 end
634 638 when 'version'
635 639 if version = Version.visible.find_by_id(oid)
636 640 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
637 641 :class => 'version'
638 642 end
639 643 when 'message'
640 644 if message = Message.visible.find_by_id(oid, :include => :parent)
641 645 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
642 646 end
643 647 when 'project'
644 648 if p = Project.visible.find_by_id(oid)
645 649 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
646 650 end
647 651 end
648 652 elsif sep == ':'
649 653 # removes the double quotes if any
650 654 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
651 655 case prefix
652 656 when 'document'
653 657 if project && document = project.documents.visible.find_by_title(name)
654 658 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
655 659 :class => 'document'
656 660 end
657 661 when 'version'
658 662 if project && version = project.versions.visible.find_by_name(name)
659 663 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
660 664 :class => 'version'
661 665 end
662 666 when 'commit'
663 667 if project && project.repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", project.repository.id, "#{name}%"]))
664 668 link = link_to h("#{project_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.identifier},
665 669 :class => 'changeset',
666 670 :title => truncate_single_line(changeset.comments, :length => 100)
667 671 end
668 672 when 'source', 'export'
669 673 if project && project.repository && User.current.allowed_to?(:browse_repository, project)
670 674 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
671 675 path, rev, anchor = $1, $3, $5
672 676 link = link_to h("#{project_prefix}#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
673 677 :path => to_path_param(path),
674 678 :rev => rev,
675 679 :anchor => anchor,
676 680 :format => (prefix == 'export' ? 'raw' : nil)},
677 681 :class => (prefix == 'export' ? 'source download' : 'source')
678 682 end
679 683 when 'attachment'
680 684 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
681 685 if attachments && attachment = attachments.detect {|a| a.filename == name }
682 686 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
683 687 :class => 'attachment'
684 688 end
685 689 when 'project'
686 690 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
687 691 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
688 692 end
689 693 end
690 694 end
691 695 end
692 696 leading + (link || "#{project_prefix}#{prefix}#{sep}#{identifier}")
693 697 end
694 698 end
695 699
696 700 HEADING_RE = /<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>/i unless const_defined?(:HEADING_RE)
697 701
698 702 # Headings and TOC
699 703 # Adds ids and links to headings unless options[:headings] is set to false
700 704 def parse_headings(text, project, obj, attr, only_path, options)
701 705 return if options[:headings] == false
702 706
703 707 text.gsub!(HEADING_RE) do
704 708 level, attrs, content = $1.to_i, $2, $3
705 709 item = strip_tags(content).strip
706 710 anchor = item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
707 711 @parsed_headings << [level, anchor, item]
708 712 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
709 713 end
710 714 end
711 715
712 716 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
713 717
714 718 # Renders the TOC with given headings
715 719 def replace_toc(text, headings)
716 720 text.gsub!(TOC_RE) do
717 721 if headings.empty?
718 722 ''
719 723 else
720 724 div_class = 'toc'
721 725 div_class << ' right' if $1 == '>'
722 726 div_class << ' left' if $1 == '<'
723 727 out = "<ul class=\"#{div_class}\"><li>"
724 728 root = headings.map(&:first).min
725 729 current = root
726 730 started = false
727 731 headings.each do |level, anchor, item|
728 732 if level > current
729 733 out << '<ul><li>' * (level - current)
730 734 elsif level < current
731 735 out << "</li></ul>\n" * (current - level) + "</li><li>"
732 736 elsif started
733 737 out << '</li><li>'
734 738 end
735 739 out << "<a href=\"##{anchor}\">#{item}</a>"
736 740 current = level
737 741 started = true
738 742 end
739 743 out << '</li></ul>' * (current - root)
740 744 out << '</li></ul>'
741 745 end
742 746 end
743 747 end
744 748
745 749 # Same as Rails' simple_format helper without using paragraphs
746 750 def simple_format_without_paragraph(text)
747 751 text.to_s.
748 752 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
749 753 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
750 754 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
751 755 end
752 756
753 757 def lang_options_for_select(blank=true)
754 758 (blank ? [["(auto)", ""]] : []) +
755 759 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
756 760 end
757 761
758 762 def label_tag_for(name, option_tags = nil, options = {})
759 763 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
760 764 content_tag("label", label_text)
761 765 end
762 766
763 767 def labelled_tabular_form_for(name, object, options, &proc)
764 768 options[:html] ||= {}
765 769 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
766 770 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
767 771 end
768 772
769 773 def back_url_hidden_field_tag
770 774 back_url = params[:back_url] || request.env['HTTP_REFERER']
771 775 back_url = CGI.unescape(back_url.to_s)
772 776 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
773 777 end
774 778
775 779 def check_all_links(form_name)
776 780 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
777 781 " | " +
778 782 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
779 783 end
780 784
781 785 def progress_bar(pcts, options={})
782 786 pcts = [pcts, pcts] unless pcts.is_a?(Array)
783 787 pcts = pcts.collect(&:round)
784 788 pcts[1] = pcts[1] - pcts[0]
785 789 pcts << (100 - pcts[1] - pcts[0])
786 790 width = options[:width] || '100px;'
787 791 legend = options[:legend] || ''
788 792 content_tag('table',
789 793 content_tag('tr',
790 794 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : '') +
791 795 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : '') +
792 796 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : '')
793 797 ), :class => 'progress', :style => "width: #{width};") +
794 798 content_tag('p', legend, :class => 'pourcent')
795 799 end
796 800
797 801 def checked_image(checked=true)
798 802 if checked
799 803 image_tag 'toggle_check.png'
800 804 end
801 805 end
802 806
803 807 def context_menu(url)
804 808 unless @context_menu_included
805 809 content_for :header_tags do
806 810 javascript_include_tag('context_menu') +
807 811 stylesheet_link_tag('context_menu')
808 812 end
809 813 if l(:direction) == 'rtl'
810 814 content_for :header_tags do
811 815 stylesheet_link_tag('context_menu_rtl')
812 816 end
813 817 end
814 818 @context_menu_included = true
815 819 end
816 820 javascript_tag "new ContextMenu('#{ url_for(url) }')"
817 821 end
818 822
819 823 def context_menu_link(name, url, options={})
820 824 options[:class] ||= ''
821 825 if options.delete(:selected)
822 826 options[:class] << ' icon-checked disabled'
823 827 options[:disabled] = true
824 828 end
825 829 if options.delete(:disabled)
826 830 options.delete(:method)
827 831 options.delete(:confirm)
828 832 options.delete(:onclick)
829 833 options[:class] << ' disabled'
830 834 url = '#'
831 835 end
832 836 link_to name, url, options
833 837 end
834 838
835 839 def calendar_for(field_id)
836 840 include_calendar_headers_tags
837 841 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
838 842 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
839 843 end
840 844
841 845 def include_calendar_headers_tags
842 846 unless @calendar_headers_tags_included
843 847 @calendar_headers_tags_included = true
844 848 content_for :header_tags do
845 849 start_of_week = case Setting.start_of_week.to_i
846 850 when 1
847 851 'Calendar._FD = 1;' # Monday
848 852 when 7
849 853 'Calendar._FD = 0;' # Sunday
850 854 when 6
851 855 'Calendar._FD = 6;' # Saturday
852 856 else
853 857 '' # use language
854 858 end
855 859
856 860 javascript_include_tag('calendar/calendar') +
857 861 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
858 862 javascript_tag(start_of_week) +
859 863 javascript_include_tag('calendar/calendar-setup') +
860 864 stylesheet_link_tag('calendar')
861 865 end
862 866 end
863 867 end
864 868
865 869 def content_for(name, content = nil, &block)
866 870 @has_content ||= {}
867 871 @has_content[name] = true
868 872 super(name, content, &block)
869 873 end
870 874
871 875 def has_content?(name)
872 876 (@has_content && @has_content[name]) || false
873 877 end
874 878
875 879 # Returns the avatar image tag for the given +user+ if avatars are enabled
876 880 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
877 881 def avatar(user, options = { })
878 882 if Setting.gravatar_enabled?
879 883 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
880 884 email = nil
881 885 if user.respond_to?(:mail)
882 886 email = user.mail
883 887 elsif user.to_s =~ %r{<(.+?)>}
884 888 email = $1
885 889 end
886 890 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
887 891 else
888 892 ''
889 893 end
890 894 end
891 895
892 896 # Returns the javascript tags that are included in the html layout head
893 897 def javascript_heads
894 898 tags = javascript_include_tag(:defaults)
895 899 unless User.current.pref.warn_on_leaving_unsaved == '0'
896 900 tags << "\n" + javascript_tag("Event.observe(window, 'load', function(){ new WarnLeavingUnsaved('#{escape_javascript( l(:text_warn_on_leaving_unsaved) )}'); });")
897 901 end
898 902 tags
899 903 end
900 904
901 905 def favicon
902 906 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />"
903 907 end
904 908
905 909 def robot_exclusion_tag
906 910 '<meta name="robots" content="noindex,follow,noarchive" />'
907 911 end
908 912
909 913 # Returns true if arg is expected in the API response
910 914 def include_in_api_response?(arg)
911 915 unless @included_in_api_response
912 916 param = params[:include]
913 917 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
914 918 @included_in_api_response.collect!(&:strip)
915 919 end
916 920 @included_in_api_response.include?(arg.to_s)
917 921 end
918 922
919 923 # Returns options or nil if nometa param or X-Redmine-Nometa header
920 924 # was set in the request
921 925 def api_meta(options)
922 926 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
923 927 # compatibility mode for activeresource clients that raise
924 928 # an error when unserializing an array with attributes
925 929 nil
926 930 else
927 931 options
928 932 end
929 933 end
930 934
931 935 private
932 936
933 937 def wiki_helper
934 938 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
935 939 extend helper
936 940 return self
937 941 end
938 942
939 943 def link_to_content_update(text, url_params = {}, html_options = {})
940 944 link_to(text, url_params, html_options)
941 945 end
942 946 end
@@ -1,678 +1,705
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 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', :r => 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_cross_project_redmine_links
229 229 source_link = link_to('ecookbook:source:/some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']},
230 230 :class => 'source')
231 231
232 232 changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
233 233 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
234 234
235 235 to_test = {
236 236 # documents
237 237 'document:"Test document"' => 'document:"Test document"',
238 238 'ecookbook:document:"Test document"' => '<a href="/documents/1" class="document">Test document</a>',
239 239 'invalid:document:"Test document"' => 'invalid:document:"Test document"',
240 240 # versions
241 241 'version:"1.0"' => 'version:"1.0"',
242 242 'ecookbook:version:"1.0"' => '<a href="/versions/show/2" class="version">1.0</a>',
243 243 'invalid:version:"1.0"' => 'invalid:version:"1.0"',
244 244 # changeset
245 245 'r2' => 'r2',
246 246 'ecookbook:r2' => changeset_link,
247 247 'invalid:r2' => 'invalid:r2',
248 248 # source
249 249 'source:/some/file' => 'source:/some/file',
250 250 'ecookbook:source:/some/file' => source_link,
251 251 'invalid:source:/some/file' => 'invalid:source:/some/file',
252 252 }
253 253 @project = Project.find(3)
254 254 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
255 255 end
256 256
257 257 def test_redmine_links_git_commit
258 258 changeset_link = link_to('abcd',
259 259 {
260 260 :controller => 'repositories',
261 261 :action => 'revision',
262 262 :id => 'subproject1',
263 263 :rev => 'abcd',
264 264 },
265 265 :class => 'changeset', :title => 'test commit')
266 266 to_test = {
267 267 'commit:abcd' => changeset_link,
268 268 }
269 269 @project = Project.find(3)
270 270 r = Repository::Git.create!(:project => @project, :url => '/tmp/test/git')
271 271 assert r
272 272 c = Changeset.new(:repository => r,
273 273 :committed_on => Time.now,
274 274 :revision => 'abcd',
275 275 :scmid => 'abcd',
276 276 :comments => 'test commit')
277 277 assert( c.save )
278 278 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
279 279 end
280 280
281 281 # TODO: Bazaar commit id contains mail address, so it contains '@' and '_'.
282 282 def test_redmine_links_darcs_commit
283 283 changeset_link = link_to('20080308225258-98289-abcd456efg.gz',
284 284 {
285 285 :controller => 'repositories',
286 286 :action => 'revision',
287 287 :id => 'subproject1',
288 288 :rev => '123',
289 289 },
290 290 :class => 'changeset', :title => 'test commit')
291 291 to_test = {
292 292 'commit:20080308225258-98289-abcd456efg.gz' => changeset_link,
293 293 }
294 294 @project = Project.find(3)
295 295 r = Repository::Darcs.create!(
296 296 :project => @project, :url => '/tmp/test/darcs',
297 297 :log_encoding => 'UTF-8')
298 298 assert r
299 299 c = Changeset.new(:repository => r,
300 300 :committed_on => Time.now,
301 301 :revision => '123',
302 302 :scmid => '20080308225258-98289-abcd456efg.gz',
303 303 :comments => 'test commit')
304 304 assert( c.save )
305 305 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
306 306 end
307 307
308 308 def test_redmine_links_mercurial_commit
309 309 changeset_link_rev = link_to('r123',
310 310 {
311 311 :controller => 'repositories',
312 312 :action => 'revision',
313 313 :id => 'subproject1',
314 314 :rev => '123' ,
315 315 },
316 316 :class => 'changeset', :title => 'test commit')
317 317 changeset_link_commit = link_to('abcd',
318 318 {
319 319 :controller => 'repositories',
320 320 :action => 'revision',
321 321 :id => 'subproject1',
322 322 :rev => 'abcd' ,
323 323 },
324 324 :class => 'changeset', :title => 'test commit')
325 325 to_test = {
326 326 'r123' => changeset_link_rev,
327 327 'commit:abcd' => changeset_link_commit,
328 328 }
329 329 @project = Project.find(3)
330 330 r = Repository::Mercurial.create!(:project => @project, :url => '/tmp/test')
331 331 assert r
332 332 c = Changeset.new(:repository => r,
333 333 :committed_on => Time.now,
334 334 :revision => '123',
335 335 :scmid => 'abcd',
336 336 :comments => 'test commit')
337 337 assert( c.save )
338 338 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
339 339 end
340 340
341 341 def test_attachment_links
342 342 attachment_link = link_to('error281.txt', {:controller => 'attachments', :action => 'download', :id => '1'}, :class => 'attachment')
343 343 to_test = {
344 344 'attachment:error281.txt' => attachment_link
345 345 }
346 346 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => Issue.find(3).attachments), "#{text} failed" }
347 347 end
348 348
349 349 def test_wiki_links
350 350 to_test = {
351 351 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
352 352 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
353 353 # link with anchor
354 354 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
355 355 '[[Another page#anchor|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page#anchor" class="wiki-page">Page</a>',
356 356 # page that doesn't exist
357 357 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
358 358 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">404</a>',
359 359 # link to another project wiki
360 360 '[[onlinestore:]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">onlinestore</a>',
361 361 '[[onlinestore:|Wiki]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">Wiki</a>',
362 362 '[[onlinestore:Start page]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Start page</a>',
363 363 '[[onlinestore:Start page|Text]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Text</a>',
364 364 '[[onlinestore:Unknown page]]' => '<a href="/projects/onlinestore/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
365 365 # striked through link
366 366 '-[[Another page|Page]]-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a></del>',
367 367 '-[[Another page|Page]] link-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a> link</del>',
368 368 # escaping
369 369 '![[Another page|Page]]' => '[[Another page|Page]]',
370 370 # project does not exist
371 371 '[[unknowproject:Start]]' => '[[unknowproject:Start]]',
372 372 '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]',
373 373 }
374 374 @project = Project.find(1)
375 375 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
376 376 end
377 377
378 378 def test_html_tags
379 379 to_test = {
380 380 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
381 381 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
382 382 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
383 383 # do not escape pre/code tags
384 384 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
385 385 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
386 386 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
387 387 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
388 388 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
389 389 # remove attributes except class
390 390 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
391 391 '<pre class="foo">some text</pre>' => '<pre class="foo">some text</pre>',
392 392 "<pre class='foo bar'>some text</pre>" => "<pre class='foo bar'>some text</pre>",
393 393 '<pre class="foo bar">some text</pre>' => '<pre class="foo bar">some text</pre>',
394 394 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
395 395 # xss
396 396 '<pre><code class=""onmouseover="alert(1)">text</code></pre>' => '<pre><code>text</code></pre>',
397 397 '<pre class=""onmouseover="alert(1)">text</pre>' => '<pre>text</pre>',
398 398 }
399 399 to_test.each { |text, result| assert_equal result, textilizable(text) }
400 400 end
401 401
402 402 def test_allowed_html_tags
403 403 to_test = {
404 404 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
405 405 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
406 406 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
407 407 }
408 408 to_test.each { |text, result| assert_equal result, textilizable(text) }
409 409 end
410 410
411 411 def test_pre_tags
412 412 raw = <<-RAW
413 413 Before
414 414
415 415 <pre>
416 416 <prepared-statement-cache-size>32</prepared-statement-cache-size>
417 417 </pre>
418 418
419 419 After
420 420 RAW
421 421
422 422 expected = <<-EXPECTED
423 423 <p>Before</p>
424 424 <pre>
425 425 &lt;prepared-statement-cache-size&gt;32&lt;/prepared-statement-cache-size&gt;
426 426 </pre>
427 427 <p>After</p>
428 428 EXPECTED
429 429
430 430 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
431 431 end
432 432
433 433 def test_pre_content_should_not_parse_wiki_and_redmine_links
434 434 raw = <<-RAW
435 435 [[CookBook documentation]]
436 436
437 437 #1
438 438
439 439 <pre>
440 440 [[CookBook documentation]]
441 441
442 442 #1
443 443 </pre>
444 444 RAW
445 445
446 446 expected = <<-EXPECTED
447 447 <p><a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a></p>
448 448 <p><a href="/issues/1" class="issue status-1 priority-1" title="Can't print recipes (New)">#1</a></p>
449 449 <pre>
450 450 [[CookBook documentation]]
451 451
452 452 #1
453 453 </pre>
454 454 EXPECTED
455 455
456 456 @project = Project.find(1)
457 457 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
458 458 end
459 459
460 460 def test_non_closing_pre_blocks_should_be_closed
461 461 raw = <<-RAW
462 462 <pre><code>
463 463 RAW
464 464
465 465 expected = <<-EXPECTED
466 466 <pre><code>
467 467 </code></pre>
468 468 EXPECTED
469 469
470 470 @project = Project.find(1)
471 471 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
472 472 end
473 473
474 474 def test_syntax_highlight
475 475 raw = <<-RAW
476 476 <pre><code class="ruby">
477 477 # Some ruby code here
478 478 </code></pre>
479 479 RAW
480 480
481 481 expected = <<-EXPECTED
482 482 <pre><code class="ruby syntaxhl"><span class=\"CodeRay\"><span class="no">1</span> <span class="c"># Some ruby code here</span></span>
483 483 </code></pre>
484 484 EXPECTED
485 485
486 486 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
487 487 end
488 488
489 489 def test_wiki_links_in_tables
490 490 to_test = {"|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" =>
491 491 '<tr><td><a href="/projects/ecookbook/wiki/Page" class="wiki-page new">Link title</a></td>' +
492 492 '<td><a href="/projects/ecookbook/wiki/Other_Page" class="wiki-page new">Other title</a></td>' +
493 493 '</tr><tr><td>Cell 21</td><td><a href="/projects/ecookbook/wiki/Last_page" class="wiki-page new">Last page</a></td></tr>'
494 494 }
495 495 @project = Project.find(1)
496 496 to_test.each { |text, result| assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '') }
497 497 end
498 498
499 499 def test_text_formatting
500 500 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
501 501 '(_text within parentheses_)' => '(<em>text within parentheses</em>)',
502 502 'a *Humane Web* Text Generator' => 'a <strong>Humane Web</strong> Text Generator',
503 503 '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>',
504 504 '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',
505 505 }
506 506 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
507 507 end
508 508
509 509 def test_wiki_horizontal_rule
510 510 assert_equal '<hr />', textilizable('---')
511 511 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
512 512 end
513 513
514 514 def test_footnotes
515 515 raw = <<-RAW
516 516 This is some text[1].
517 517
518 518 fn1. This is the foot note
519 519 RAW
520 520
521 521 expected = <<-EXPECTED
522 522 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
523 523 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
524 524 EXPECTED
525 525
526 526 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
527 527 end
528 528
529 529 def test_headings
530 530 raw = 'h1. Some heading'
531 531 expected = %|<a name="Some-heading"></a>\n<h1 >Some heading<a href="#Some-heading" class="wiki-anchor">&para;</a></h1>|
532 532
533 533 assert_equal expected, textilizable(raw)
534 534 end
535 535
536 def test_wiki_links_within_wiki_page_context
537
538 page = WikiPage.find_by_title('Another_page' )
539
540 to_test = {
541 # link to another page
542 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
543 '[[CookBook documentation|documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">documentation</a>',
544 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
545 '[[CookBook documentation#One-section|documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">documentation</a>',
546 # link to the current page
547 '[[Another page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Another page</a>',
548 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
549 '[[Another page#anchor]]' => '<a href="#anchor" class="wiki-page">Another page</a>',
550 '[[Another page#anchor|Page]]' => '<a href="#anchor" class="wiki-page">Page</a>',
551 # page that doesn't exist
552 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
553 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">404</a>',
554 '[[Unknown page#anchor]]' => '<a href="/projects/ecookbook/wiki/Unknown_page#anchor" class="wiki-page new">Unknown page</a>',
555 '[[Unknown page#anchor|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page#anchor" class="wiki-page new">404</a>',
556 }
557
558 @project = Project.find(1)
559
560 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(WikiContent.generate!( :text => text, :page => page ), :text) }
561 end
562
536 563 def test_table_of_content
537 564 raw = <<-RAW
538 565 {{toc}}
539 566
540 567 h1. Title
541 568
542 569 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
543 570
544 571 h2. Subtitle with a [[Wiki]] link
545 572
546 573 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
547 574
548 575 h2. Subtitle with [[Wiki|another Wiki]] link
549 576
550 577 h2. Subtitle with %{color:red}red text%
551 578
552 579 <pre>
553 580 some code
554 581 </pre>
555 582
556 583 h3. Subtitle with *some* _modifiers_
557 584
558 585 h1. Another title
559 586
560 587 h3. An "Internet link":http://www.redmine.org/ inside subtitle
561 588
562 589 h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
563 590
564 591 RAW
565 592
566 593 expected = '<ul class="toc">' +
567 594 '<li><a href="#Title">Title</a>' +
568 595 '<ul>' +
569 596 '<li><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
570 597 '<li><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
571 598 '<li><a href="#Subtitle-with-red-text">Subtitle with red text</a>' +
572 599 '<ul>' +
573 600 '<li><a href="#Subtitle-with-some-modifiers">Subtitle with some modifiers</a></li>' +
574 601 '</ul>' +
575 602 '</li>' +
576 603 '</ul>' +
577 604 '</li>' +
578 605 '<li><a href="#Another-title">Another title</a>' +
579 606 '<ul>' +
580 607 '<li>' +
581 608 '<ul>' +
582 609 '<li><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
583 610 '</ul>' +
584 611 '</li>' +
585 612 '<li><a href="#Project-Name">Project Name</a></li>' +
586 613 '</ul>' +
587 614 '</li>' +
588 615 '</ul>'
589 616
590 617 @project = Project.find(1)
591 618 assert textilizable(raw).gsub("\n", "").include?(expected), textilizable(raw)
592 619 end
593 620
594 621 def test_table_of_content_should_contain_included_page_headings
595 622 raw = <<-RAW
596 623 {{toc}}
597 624
598 625 h1. Included
599 626
600 627 {{include(Child_1)}}
601 628 RAW
602 629
603 630 expected = '<ul class="toc">' +
604 631 '<li><a href="#Included">Included</a></li>' +
605 632 '<li><a href="#Child-page-1">Child page 1</a></li>' +
606 633 '</ul>'
607 634
608 635 @project = Project.find(1)
609 636 assert textilizable(raw).gsub("\n", "").include?(expected)
610 637 end
611 638
612 639 def test_default_formatter
613 640 Setting.text_formatting = 'unknown'
614 641 text = 'a *link*: http://www.example.net/'
615 642 assert_equal '<p>a *link*: <a href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
616 643 Setting.text_formatting = 'textile'
617 644 end
618 645
619 646 def test_due_date_distance_in_words
620 647 to_test = { Date.today => 'Due in 0 days',
621 648 Date.today + 1 => 'Due in 1 day',
622 649 Date.today + 100 => 'Due in about 3 months',
623 650 Date.today + 20000 => 'Due in over 54 years',
624 651 Date.today - 1 => '1 day late',
625 652 Date.today - 100 => 'about 3 months late',
626 653 Date.today - 20000 => 'over 54 years late',
627 654 }
628 655 ::I18n.locale = :en
629 656 to_test.each do |date, expected|
630 657 assert_equal expected, due_date_distance_in_words(date)
631 658 end
632 659 end
633 660
634 661 def test_avatar
635 662 # turn on avatars
636 663 Setting.gravatar_enabled = '1'
637 664 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
638 665 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
639 666 assert_nil avatar('jsmith')
640 667 assert_nil avatar(nil)
641 668
642 669 # turn off avatars
643 670 Setting.gravatar_enabled = '0'
644 671 assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
645 672 end
646 673
647 674 def test_link_to_user
648 675 user = User.find(2)
649 676 t = link_to_user(user)
650 677 assert_equal "<a href=\"/users/2\">#{ user.name }</a>", t
651 678 end
652 679
653 680 def test_link_to_user_should_not_link_to_locked_user
654 681 user = User.find(5)
655 682 assert user.locked?
656 683 t = link_to_user(user)
657 684 assert_equal user.name, t
658 685 end
659 686
660 687 def test_link_to_user_should_not_link_to_anonymous
661 688 user = User.anonymous
662 689 assert user.anonymous?
663 690 t = link_to_user(user)
664 691 assert_equal ::I18n.t(:label_user_anonymous), t
665 692 end
666 693
667 694 def test_link_to_project
668 695 project = Project.find(1)
669 696 assert_equal %(<a href="/projects/ecookbook">eCookbook</a>),
670 697 link_to_project(project)
671 698 assert_equal %(<a href="/projects/ecookbook/settings">eCookbook</a>),
672 699 link_to_project(project, :action => 'settings')
673 700 assert_equal %(<a href="http://test.host/projects/ecookbook?jump=blah">eCookbook</a>),
674 701 link_to_project(project, {:only_path => false, :jump => 'blah'})
675 702 assert_equal %(<a href="/projects/ecookbook/settings" class="project">eCookbook</a>),
676 703 link_to_project(project, {:action => 'settings'}, :class => "project")
677 704 end
678 705 end
General Comments 0
You need to be logged in to leave comments. Login now