##// END OF EJS Templates
Merged #7560 from trunk....
Etienne Massip -
r7441:8158a26ee800
parent child
Show More
@@ -1,946 +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 555 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
556 556 "##{anchor}"
557 557 else
558 558 case options[:wiki_links]
559 when :local; "#{title}.html"
559 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
560 560 when :anchor; "##{title}" # used for single-file wiki export
561 561 else
562 562 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
563 563 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, :id => wiki_page_id, :anchor => anchor)
564 564 end
565 565 end
566 566 link_to((title || page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
567 567 else
568 568 # project or wiki doesn't exist
569 569 all
570 570 end
571 571 else
572 572 all
573 573 end
574 574 end
575 575 end
576 576
577 577 # Redmine links
578 578 #
579 579 # Examples:
580 580 # Issues:
581 581 # #52 -> Link to issue #52
582 582 # Changesets:
583 583 # r52 -> Link to revision 52
584 584 # commit:a85130f -> Link to scmid starting with a85130f
585 585 # Documents:
586 586 # document#17 -> Link to document with id 17
587 587 # document:Greetings -> Link to the document with title "Greetings"
588 588 # document:"Some document" -> Link to the document with title "Some document"
589 589 # Versions:
590 590 # version#3 -> Link to version with id 3
591 591 # version:1.0.0 -> Link to version named "1.0.0"
592 592 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
593 593 # Attachments:
594 594 # attachment:file.zip -> Link to the attachment of the current object named file.zip
595 595 # Source files:
596 596 # source:some/file -> Link to the file located at /some/file in the project's repository
597 597 # source:some/file@52 -> Link to the file's revision 52
598 598 # source:some/file#L120 -> Link to line 120 of the file
599 599 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
600 600 # export:some/file -> Force the download of the file
601 601 # Forum messages:
602 602 # message#1218 -> Link to message with id 1218
603 603 #
604 604 # Links can refer other objects from other projects, using project identifier:
605 605 # identifier:r52
606 606 # identifier:document:"Some document"
607 607 # identifier:version:1.0.0
608 608 # identifier:source:some/file
609 609 def parse_redmine_links(text, project, obj, attr, only_path, options)
610 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|
611 611 leading, esc, project_prefix, project_identifier, prefix, sep, identifier = $1, $2, $3, $4, $5, $7 || $9, $8 || $10
612 612 link = nil
613 613 if project_identifier
614 614 project = Project.visible.find_by_identifier(project_identifier)
615 615 end
616 616 if esc.nil?
617 617 if prefix.nil? && sep == 'r'
618 618 # project.changesets.visible raises an SQL error because of a double join on repositories
619 619 if project && project.repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(project.repository.id, identifier))
620 620 link = link_to("#{project_prefix}r#{identifier}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
621 621 :class => 'changeset',
622 622 :title => truncate_single_line(changeset.comments, :length => 100))
623 623 end
624 624 elsif sep == '#'
625 625 oid = identifier.to_i
626 626 case prefix
627 627 when nil
628 628 if issue = Issue.visible.find_by_id(oid, :include => :status)
629 629 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
630 630 :class => issue.css_classes,
631 631 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
632 632 end
633 633 when 'document'
634 634 if document = Document.visible.find_by_id(oid)
635 635 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
636 636 :class => 'document'
637 637 end
638 638 when 'version'
639 639 if version = Version.visible.find_by_id(oid)
640 640 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
641 641 :class => 'version'
642 642 end
643 643 when 'message'
644 644 if message = Message.visible.find_by_id(oid, :include => :parent)
645 645 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
646 646 end
647 647 when 'project'
648 648 if p = Project.visible.find_by_id(oid)
649 649 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
650 650 end
651 651 end
652 652 elsif sep == ':'
653 653 # removes the double quotes if any
654 654 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
655 655 case prefix
656 656 when 'document'
657 657 if project && document = project.documents.visible.find_by_title(name)
658 658 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
659 659 :class => 'document'
660 660 end
661 661 when 'version'
662 662 if project && version = project.versions.visible.find_by_name(name)
663 663 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
664 664 :class => 'version'
665 665 end
666 666 when 'commit'
667 667 if project && project.repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", project.repository.id, "#{name}%"]))
668 668 link = link_to h("#{project_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.identifier},
669 669 :class => 'changeset',
670 670 :title => truncate_single_line(changeset.comments, :length => 100)
671 671 end
672 672 when 'source', 'export'
673 673 if project && project.repository && User.current.allowed_to?(:browse_repository, project)
674 674 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
675 675 path, rev, anchor = $1, $3, $5
676 676 link = link_to h("#{project_prefix}#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
677 677 :path => to_path_param(path),
678 678 :rev => rev,
679 679 :anchor => anchor,
680 680 :format => (prefix == 'export' ? 'raw' : nil)},
681 681 :class => (prefix == 'export' ? 'source download' : 'source')
682 682 end
683 683 when 'attachment'
684 684 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
685 685 if attachments && attachment = attachments.detect {|a| a.filename == name }
686 686 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
687 687 :class => 'attachment'
688 688 end
689 689 when 'project'
690 690 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
691 691 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
692 692 end
693 693 end
694 694 end
695 695 end
696 696 leading + (link || "#{project_prefix}#{prefix}#{sep}#{identifier}")
697 697 end
698 698 end
699 699
700 700 HEADING_RE = /<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>/i unless const_defined?(:HEADING_RE)
701 701
702 702 # Headings and TOC
703 703 # Adds ids and links to headings unless options[:headings] is set to false
704 704 def parse_headings(text, project, obj, attr, only_path, options)
705 705 return if options[:headings] == false
706 706
707 707 text.gsub!(HEADING_RE) do
708 708 level, attrs, content = $1.to_i, $2, $3
709 709 item = strip_tags(content).strip
710 710 anchor = item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
711 711 @parsed_headings << [level, anchor, item]
712 712 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
713 713 end
714 714 end
715 715
716 716 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
717 717
718 718 # Renders the TOC with given headings
719 719 def replace_toc(text, headings)
720 720 text.gsub!(TOC_RE) do
721 721 if headings.empty?
722 722 ''
723 723 else
724 724 div_class = 'toc'
725 725 div_class << ' right' if $1 == '>'
726 726 div_class << ' left' if $1 == '<'
727 727 out = "<ul class=\"#{div_class}\"><li>"
728 728 root = headings.map(&:first).min
729 729 current = root
730 730 started = false
731 731 headings.each do |level, anchor, item|
732 732 if level > current
733 733 out << '<ul><li>' * (level - current)
734 734 elsif level < current
735 735 out << "</li></ul>\n" * (current - level) + "</li><li>"
736 736 elsif started
737 737 out << '</li><li>'
738 738 end
739 739 out << "<a href=\"##{anchor}\">#{item}</a>"
740 740 current = level
741 741 started = true
742 742 end
743 743 out << '</li></ul>' * (current - root)
744 744 out << '</li></ul>'
745 745 end
746 746 end
747 747 end
748 748
749 749 # Same as Rails' simple_format helper without using paragraphs
750 750 def simple_format_without_paragraph(text)
751 751 text.to_s.
752 752 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
753 753 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
754 754 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
755 755 end
756 756
757 757 def lang_options_for_select(blank=true)
758 758 (blank ? [["(auto)", ""]] : []) +
759 759 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
760 760 end
761 761
762 762 def label_tag_for(name, option_tags = nil, options = {})
763 763 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
764 764 content_tag("label", label_text)
765 765 end
766 766
767 767 def labelled_tabular_form_for(name, object, options, &proc)
768 768 options[:html] ||= {}
769 769 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
770 770 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
771 771 end
772 772
773 773 def back_url_hidden_field_tag
774 774 back_url = params[:back_url] || request.env['HTTP_REFERER']
775 775 back_url = CGI.unescape(back_url.to_s)
776 776 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
777 777 end
778 778
779 779 def check_all_links(form_name)
780 780 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
781 781 " | " +
782 782 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
783 783 end
784 784
785 785 def progress_bar(pcts, options={})
786 786 pcts = [pcts, pcts] unless pcts.is_a?(Array)
787 787 pcts = pcts.collect(&:round)
788 788 pcts[1] = pcts[1] - pcts[0]
789 789 pcts << (100 - pcts[1] - pcts[0])
790 790 width = options[:width] || '100px;'
791 791 legend = options[:legend] || ''
792 792 content_tag('table',
793 793 content_tag('tr',
794 794 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : '') +
795 795 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : '') +
796 796 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : '')
797 797 ), :class => 'progress', :style => "width: #{width};") +
798 798 content_tag('p', legend, :class => 'pourcent')
799 799 end
800 800
801 801 def checked_image(checked=true)
802 802 if checked
803 803 image_tag 'toggle_check.png'
804 804 end
805 805 end
806 806
807 807 def context_menu(url)
808 808 unless @context_menu_included
809 809 content_for :header_tags do
810 810 javascript_include_tag('context_menu') +
811 811 stylesheet_link_tag('context_menu')
812 812 end
813 813 if l(:direction) == 'rtl'
814 814 content_for :header_tags do
815 815 stylesheet_link_tag('context_menu_rtl')
816 816 end
817 817 end
818 818 @context_menu_included = true
819 819 end
820 820 javascript_tag "new ContextMenu('#{ url_for(url) }')"
821 821 end
822 822
823 823 def context_menu_link(name, url, options={})
824 824 options[:class] ||= ''
825 825 if options.delete(:selected)
826 826 options[:class] << ' icon-checked disabled'
827 827 options[:disabled] = true
828 828 end
829 829 if options.delete(:disabled)
830 830 options.delete(:method)
831 831 options.delete(:confirm)
832 832 options.delete(:onclick)
833 833 options[:class] << ' disabled'
834 834 url = '#'
835 835 end
836 836 link_to name, url, options
837 837 end
838 838
839 839 def calendar_for(field_id)
840 840 include_calendar_headers_tags
841 841 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
842 842 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
843 843 end
844 844
845 845 def include_calendar_headers_tags
846 846 unless @calendar_headers_tags_included
847 847 @calendar_headers_tags_included = true
848 848 content_for :header_tags do
849 849 start_of_week = case Setting.start_of_week.to_i
850 850 when 1
851 851 'Calendar._FD = 1;' # Monday
852 852 when 7
853 853 'Calendar._FD = 0;' # Sunday
854 854 when 6
855 855 'Calendar._FD = 6;' # Saturday
856 856 else
857 857 '' # use language
858 858 end
859 859
860 860 javascript_include_tag('calendar/calendar') +
861 861 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
862 862 javascript_tag(start_of_week) +
863 863 javascript_include_tag('calendar/calendar-setup') +
864 864 stylesheet_link_tag('calendar')
865 865 end
866 866 end
867 867 end
868 868
869 869 def content_for(name, content = nil, &block)
870 870 @has_content ||= {}
871 871 @has_content[name] = true
872 872 super(name, content, &block)
873 873 end
874 874
875 875 def has_content?(name)
876 876 (@has_content && @has_content[name]) || false
877 877 end
878 878
879 879 # Returns the avatar image tag for the given +user+ if avatars are enabled
880 880 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
881 881 def avatar(user, options = { })
882 882 if Setting.gravatar_enabled?
883 883 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
884 884 email = nil
885 885 if user.respond_to?(:mail)
886 886 email = user.mail
887 887 elsif user.to_s =~ %r{<(.+?)>}
888 888 email = $1
889 889 end
890 890 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
891 891 else
892 892 ''
893 893 end
894 894 end
895 895
896 896 # Returns the javascript tags that are included in the html layout head
897 897 def javascript_heads
898 898 tags = javascript_include_tag(:defaults)
899 899 unless User.current.pref.warn_on_leaving_unsaved == '0'
900 900 tags << "\n" + javascript_tag("Event.observe(window, 'load', function(){ new WarnLeavingUnsaved('#{escape_javascript( l(:text_warn_on_leaving_unsaved) )}'); });")
901 901 end
902 902 tags
903 903 end
904 904
905 905 def favicon
906 906 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />"
907 907 end
908 908
909 909 def robot_exclusion_tag
910 910 '<meta name="robots" content="noindex,follow,noarchive" />'
911 911 end
912 912
913 913 # Returns true if arg is expected in the API response
914 914 def include_in_api_response?(arg)
915 915 unless @included_in_api_response
916 916 param = params[:include]
917 917 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
918 918 @included_in_api_response.collect!(&:strip)
919 919 end
920 920 @included_in_api_response.include?(arg.to_s)
921 921 end
922 922
923 923 # Returns options or nil if nometa param or X-Redmine-Nometa header
924 924 # was set in the request
925 925 def api_meta(options)
926 926 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
927 927 # compatibility mode for activeresource clients that raise
928 928 # an error when unserializing an array with attributes
929 929 nil
930 930 else
931 931 options
932 932 end
933 933 end
934 934
935 935 private
936 936
937 937 def wiki_helper
938 938 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
939 939 extend helper
940 940 return self
941 941 end
942 942
943 943 def link_to_content_update(text, url_params = {}, html_options = {})
944 944 link_to(text, url_params, html_options)
945 945 end
946 946 end
@@ -1,705 +1,725
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 def test_wiki_links_within_local_file_generation_context
379
380 to_test = {
381 # link to a page
382 '[[CookBook documentation]]' => '<a href="CookBook_documentation.html" class="wiki-page">CookBook documentation</a>',
383 '[[CookBook documentation|documentation]]' => '<a href="CookBook_documentation.html" class="wiki-page">documentation</a>',
384 '[[CookBook documentation#One-section]]' => '<a href="CookBook_documentation.html#One-section" class="wiki-page">CookBook documentation</a>',
385 '[[CookBook documentation#One-section|documentation]]' => '<a href="CookBook_documentation.html#One-section" class="wiki-page">documentation</a>',
386 # page that doesn't exist
387 '[[Unknown page]]' => '<a href="Unknown_page.html" class="wiki-page new">Unknown page</a>',
388 '[[Unknown page|404]]' => '<a href="Unknown_page.html" class="wiki-page new">404</a>',
389 '[[Unknown page#anchor]]' => '<a href="Unknown_page.html#anchor" class="wiki-page new">Unknown page</a>',
390 '[[Unknown page#anchor|404]]' => '<a href="Unknown_page.html#anchor" class="wiki-page new">404</a>',
391 }
392
393 @project = Project.find(1)
394
395 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :local) }
396 end
397
378 398 def test_html_tags
379 399 to_test = {
380 400 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
381 401 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
382 402 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
383 403 # do not escape pre/code tags
384 404 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
385 405 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
386 406 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
387 407 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
388 408 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
389 409 # remove attributes except class
390 410 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
391 411 '<pre class="foo">some text</pre>' => '<pre class="foo">some text</pre>',
392 412 "<pre class='foo bar'>some text</pre>" => "<pre class='foo bar'>some text</pre>",
393 413 '<pre class="foo bar">some text</pre>' => '<pre class="foo bar">some text</pre>',
394 414 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
395 415 # xss
396 416 '<pre><code class=""onmouseover="alert(1)">text</code></pre>' => '<pre><code>text</code></pre>',
397 417 '<pre class=""onmouseover="alert(1)">text</pre>' => '<pre>text</pre>',
398 418 }
399 419 to_test.each { |text, result| assert_equal result, textilizable(text) }
400 420 end
401 421
402 422 def test_allowed_html_tags
403 423 to_test = {
404 424 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
405 425 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
406 426 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
407 427 }
408 428 to_test.each { |text, result| assert_equal result, textilizable(text) }
409 429 end
410 430
411 431 def test_pre_tags
412 432 raw = <<-RAW
413 433 Before
414 434
415 435 <pre>
416 436 <prepared-statement-cache-size>32</prepared-statement-cache-size>
417 437 </pre>
418 438
419 439 After
420 440 RAW
421 441
422 442 expected = <<-EXPECTED
423 443 <p>Before</p>
424 444 <pre>
425 445 &lt;prepared-statement-cache-size&gt;32&lt;/prepared-statement-cache-size&gt;
426 446 </pre>
427 447 <p>After</p>
428 448 EXPECTED
429 449
430 450 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
431 451 end
432 452
433 453 def test_pre_content_should_not_parse_wiki_and_redmine_links
434 454 raw = <<-RAW
435 455 [[CookBook documentation]]
436 456
437 457 #1
438 458
439 459 <pre>
440 460 [[CookBook documentation]]
441 461
442 462 #1
443 463 </pre>
444 464 RAW
445 465
446 466 expected = <<-EXPECTED
447 467 <p><a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a></p>
448 468 <p><a href="/issues/1" class="issue status-1 priority-1" title="Can't print recipes (New)">#1</a></p>
449 469 <pre>
450 470 [[CookBook documentation]]
451 471
452 472 #1
453 473 </pre>
454 474 EXPECTED
455 475
456 476 @project = Project.find(1)
457 477 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
458 478 end
459 479
460 480 def test_non_closing_pre_blocks_should_be_closed
461 481 raw = <<-RAW
462 482 <pre><code>
463 483 RAW
464 484
465 485 expected = <<-EXPECTED
466 486 <pre><code>
467 487 </code></pre>
468 488 EXPECTED
469 489
470 490 @project = Project.find(1)
471 491 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
472 492 end
473 493
474 494 def test_syntax_highlight
475 495 raw = <<-RAW
476 496 <pre><code class="ruby">
477 497 # Some ruby code here
478 498 </code></pre>
479 499 RAW
480 500
481 501 expected = <<-EXPECTED
482 502 <pre><code class="ruby syntaxhl"><span class=\"CodeRay\"><span class="no">1</span> <span class="c"># Some ruby code here</span></span>
483 503 </code></pre>
484 504 EXPECTED
485 505
486 506 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
487 507 end
488 508
489 509 def test_wiki_links_in_tables
490 510 to_test = {"|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" =>
491 511 '<tr><td><a href="/projects/ecookbook/wiki/Page" class="wiki-page new">Link title</a></td>' +
492 512 '<td><a href="/projects/ecookbook/wiki/Other_Page" class="wiki-page new">Other title</a></td>' +
493 513 '</tr><tr><td>Cell 21</td><td><a href="/projects/ecookbook/wiki/Last_page" class="wiki-page new">Last page</a></td></tr>'
494 514 }
495 515 @project = Project.find(1)
496 516 to_test.each { |text, result| assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '') }
497 517 end
498 518
499 519 def test_text_formatting
500 520 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
501 521 '(_text within parentheses_)' => '(<em>text within parentheses</em>)',
502 522 'a *Humane Web* Text Generator' => 'a <strong>Humane Web</strong> Text Generator',
503 523 '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 524 '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 525 }
506 526 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
507 527 end
508 528
509 529 def test_wiki_horizontal_rule
510 530 assert_equal '<hr />', textilizable('---')
511 531 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
512 532 end
513 533
514 534 def test_footnotes
515 535 raw = <<-RAW
516 536 This is some text[1].
517 537
518 538 fn1. This is the foot note
519 539 RAW
520 540
521 541 expected = <<-EXPECTED
522 542 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
523 543 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
524 544 EXPECTED
525 545
526 546 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
527 547 end
528 548
529 549 def test_headings
530 550 raw = 'h1. Some heading'
531 551 expected = %|<a name="Some-heading"></a>\n<h1 >Some heading<a href="#Some-heading" class="wiki-anchor">&para;</a></h1>|
532 552
533 553 assert_equal expected, textilizable(raw)
534 554 end
535 555
536 556 def test_wiki_links_within_wiki_page_context
537 557
538 558 page = WikiPage.find_by_title('Another_page' )
539 559
540 560 to_test = {
541 561 # link to another page
542 562 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
543 563 '[[CookBook documentation|documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">documentation</a>',
544 564 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
545 565 '[[CookBook documentation#One-section|documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">documentation</a>',
546 566 # link to the current page
547 567 '[[Another page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Another page</a>',
548 568 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
549 569 '[[Another page#anchor]]' => '<a href="#anchor" class="wiki-page">Another page</a>',
550 570 '[[Another page#anchor|Page]]' => '<a href="#anchor" class="wiki-page">Page</a>',
551 571 # page that doesn't exist
552 572 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
553 573 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">404</a>',
554 574 '[[Unknown page#anchor]]' => '<a href="/projects/ecookbook/wiki/Unknown_page#anchor" class="wiki-page new">Unknown page</a>',
555 575 '[[Unknown page#anchor|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page#anchor" class="wiki-page new">404</a>',
556 576 }
557 577
558 578 @project = Project.find(1)
559 579
560 580 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(WikiContent.generate!( :text => text, :page => page ), :text) }
561 581 end
562 582
563 583 def test_table_of_content
564 584 raw = <<-RAW
565 585 {{toc}}
566 586
567 587 h1. Title
568 588
569 589 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
570 590
571 591 h2. Subtitle with a [[Wiki]] link
572 592
573 593 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
574 594
575 595 h2. Subtitle with [[Wiki|another Wiki]] link
576 596
577 597 h2. Subtitle with %{color:red}red text%
578 598
579 599 <pre>
580 600 some code
581 601 </pre>
582 602
583 603 h3. Subtitle with *some* _modifiers_
584 604
585 605 h1. Another title
586 606
587 607 h3. An "Internet link":http://www.redmine.org/ inside subtitle
588 608
589 609 h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
590 610
591 611 RAW
592 612
593 613 expected = '<ul class="toc">' +
594 614 '<li><a href="#Title">Title</a>' +
595 615 '<ul>' +
596 616 '<li><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
597 617 '<li><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
598 618 '<li><a href="#Subtitle-with-red-text">Subtitle with red text</a>' +
599 619 '<ul>' +
600 620 '<li><a href="#Subtitle-with-some-modifiers">Subtitle with some modifiers</a></li>' +
601 621 '</ul>' +
602 622 '</li>' +
603 623 '</ul>' +
604 624 '</li>' +
605 625 '<li><a href="#Another-title">Another title</a>' +
606 626 '<ul>' +
607 627 '<li>' +
608 628 '<ul>' +
609 629 '<li><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
610 630 '</ul>' +
611 631 '</li>' +
612 632 '<li><a href="#Project-Name">Project Name</a></li>' +
613 633 '</ul>' +
614 634 '</li>' +
615 635 '</ul>'
616 636
617 637 @project = Project.find(1)
618 638 assert textilizable(raw).gsub("\n", "").include?(expected), textilizable(raw)
619 639 end
620 640
621 641 def test_table_of_content_should_contain_included_page_headings
622 642 raw = <<-RAW
623 643 {{toc}}
624 644
625 645 h1. Included
626 646
627 647 {{include(Child_1)}}
628 648 RAW
629 649
630 650 expected = '<ul class="toc">' +
631 651 '<li><a href="#Included">Included</a></li>' +
632 652 '<li><a href="#Child-page-1">Child page 1</a></li>' +
633 653 '</ul>'
634 654
635 655 @project = Project.find(1)
636 656 assert textilizable(raw).gsub("\n", "").include?(expected)
637 657 end
638 658
639 659 def test_default_formatter
640 660 Setting.text_formatting = 'unknown'
641 661 text = 'a *link*: http://www.example.net/'
642 662 assert_equal '<p>a *link*: <a href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
643 663 Setting.text_formatting = 'textile'
644 664 end
645 665
646 666 def test_due_date_distance_in_words
647 667 to_test = { Date.today => 'Due in 0 days',
648 668 Date.today + 1 => 'Due in 1 day',
649 669 Date.today + 100 => 'Due in about 3 months',
650 670 Date.today + 20000 => 'Due in over 54 years',
651 671 Date.today - 1 => '1 day late',
652 672 Date.today - 100 => 'about 3 months late',
653 673 Date.today - 20000 => 'over 54 years late',
654 674 }
655 675 ::I18n.locale = :en
656 676 to_test.each do |date, expected|
657 677 assert_equal expected, due_date_distance_in_words(date)
658 678 end
659 679 end
660 680
661 681 def test_avatar
662 682 # turn on avatars
663 683 Setting.gravatar_enabled = '1'
664 684 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
665 685 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
666 686 assert_nil avatar('jsmith')
667 687 assert_nil avatar(nil)
668 688
669 689 # turn off avatars
670 690 Setting.gravatar_enabled = '0'
671 691 assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
672 692 end
673 693
674 694 def test_link_to_user
675 695 user = User.find(2)
676 696 t = link_to_user(user)
677 697 assert_equal "<a href=\"/users/2\">#{ user.name }</a>", t
678 698 end
679 699
680 700 def test_link_to_user_should_not_link_to_locked_user
681 701 user = User.find(5)
682 702 assert user.locked?
683 703 t = link_to_user(user)
684 704 assert_equal user.name, t
685 705 end
686 706
687 707 def test_link_to_user_should_not_link_to_anonymous
688 708 user = User.anonymous
689 709 assert user.anonymous?
690 710 t = link_to_user(user)
691 711 assert_equal ::I18n.t(:label_user_anonymous), t
692 712 end
693 713
694 714 def test_link_to_project
695 715 project = Project.find(1)
696 716 assert_equal %(<a href="/projects/ecookbook">eCookbook</a>),
697 717 link_to_project(project)
698 718 assert_equal %(<a href="/projects/ecookbook/settings">eCookbook</a>),
699 719 link_to_project(project, :action => 'settings')
700 720 assert_equal %(<a href="http://test.host/projects/ecookbook?jump=blah">eCookbook</a>),
701 721 link_to_project(project, {:only_path => false, :jump => 'blah'})
702 722 assert_equal %(<a href="/projects/ecookbook/settings" class="project">eCookbook</a>),
703 723 link_to_project(project, {:action => 'settings'}, :class => "project")
704 724 end
705 725 end
General Comments 0
You need to be logged in to leave comments. Login now