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