##// END OF EJS Templates
Merged r8841 from trunk....
Jean-Philippe Lang -
r8991:488926276daa
parent child
Show More
@@ -1,1068 +1,1071
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 493 @heading_anchors = {}
494 494 @current_section = 0 if options[:edit_section_links]
495
496 parse_sections(text, project, obj, attr, only_path, options)
495 497 text = parse_non_pre_blocks(text) do |text|
496 [:parse_sections, :parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_macros, :parse_headings].each do |method_name|
498 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_macros].each do |method_name|
497 499 send method_name, text, project, obj, attr, only_path, options
498 500 end
499 501 end
502 parse_headings(text, project, obj, attr, only_path, options)
500 503
501 504 if @parsed_headings.any?
502 505 replace_toc(text, @parsed_headings)
503 506 end
504 507
505 508 text
506 509 end
507 510
508 511 def parse_non_pre_blocks(text)
509 512 s = StringScanner.new(text)
510 513 tags = []
511 514 parsed = ''
512 515 while !s.eos?
513 516 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
514 517 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
515 518 if tags.empty?
516 519 yield text
517 520 end
518 521 parsed << text
519 522 if tag
520 523 if closing
521 524 if tags.last == tag.downcase
522 525 tags.pop
523 526 end
524 527 else
525 528 tags << tag.downcase
526 529 end
527 530 parsed << full_tag
528 531 end
529 532 end
530 533 # Close any non closing tags
531 534 while tag = tags.pop
532 535 parsed << "</#{tag}>"
533 536 end
534 537 parsed.html_safe
535 538 end
536 539
537 540 def parse_inline_attachments(text, project, obj, attr, only_path, options)
538 541 # when using an image link, try to use an attachment, if possible
539 542 if options[:attachments] || (obj && obj.respond_to?(:attachments))
540 543 attachments = options[:attachments] || obj.attachments
541 544 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
542 545 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
543 546 # search for the picture in attachments
544 547 if found = Attachment.latest_attach(attachments, filename)
545 548 image_url = url_for :only_path => only_path, :controller => 'attachments',
546 549 :action => 'download', :id => found
547 550 desc = found.description.to_s.gsub('"', '')
548 551 if !desc.blank? && alttext.blank?
549 552 alt = " title=\"#{desc}\" alt=\"#{desc}\""
550 553 end
551 554 "src=\"#{image_url}\"#{alt}".html_safe
552 555 else
553 556 m.html_safe
554 557 end
555 558 end
556 559 end
557 560 end
558 561
559 562 # Wiki links
560 563 #
561 564 # Examples:
562 565 # [[mypage]]
563 566 # [[mypage|mytext]]
564 567 # wiki links can refer other project wikis, using project name or identifier:
565 568 # [[project:]] -> wiki starting page
566 569 # [[project:|mytext]]
567 570 # [[project:mypage]]
568 571 # [[project:mypage|mytext]]
569 572 def parse_wiki_links(text, project, obj, attr, only_path, options)
570 573 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
571 574 link_project = project
572 575 esc, all, page, title = $1, $2, $3, $5
573 576 if esc.nil?
574 577 if page =~ /^([^\:]+)\:(.*)$/
575 578 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
576 579 page = $2
577 580 title ||= $1 if page.blank?
578 581 end
579 582
580 583 if link_project && link_project.wiki
581 584 # extract anchor
582 585 anchor = nil
583 586 if page =~ /^(.+?)\#(.+)$/
584 587 page, anchor = $1, $2
585 588 end
586 589 anchor = sanitize_anchor_name(anchor) if anchor.present?
587 590 # check if page exists
588 591 wiki_page = link_project.wiki.find_page(page)
589 592 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
590 593 "##{anchor}"
591 594 else
592 595 case options[:wiki_links]
593 596 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
594 597 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
595 598 else
596 599 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
597 600 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, :id => wiki_page_id, :anchor => anchor)
598 601 end
599 602 end
600 603 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
601 604 else
602 605 # project or wiki doesn't exist
603 606 all.html_safe
604 607 end
605 608 else
606 609 all.html_safe
607 610 end
608 611 end
609 612 end
610 613
611 614 # Redmine links
612 615 #
613 616 # Examples:
614 617 # Issues:
615 618 # #52 -> Link to issue #52
616 619 # Changesets:
617 620 # r52 -> Link to revision 52
618 621 # commit:a85130f -> Link to scmid starting with a85130f
619 622 # Documents:
620 623 # document#17 -> Link to document with id 17
621 624 # document:Greetings -> Link to the document with title "Greetings"
622 625 # document:"Some document" -> Link to the document with title "Some document"
623 626 # Versions:
624 627 # version#3 -> Link to version with id 3
625 628 # version:1.0.0 -> Link to version named "1.0.0"
626 629 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
627 630 # Attachments:
628 631 # attachment:file.zip -> Link to the attachment of the current object named file.zip
629 632 # Source files:
630 633 # source:some/file -> Link to the file located at /some/file in the project's repository
631 634 # source:some/file@52 -> Link to the file's revision 52
632 635 # source:some/file#L120 -> Link to line 120 of the file
633 636 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
634 637 # export:some/file -> Force the download of the file
635 638 # Forum messages:
636 639 # message#1218 -> Link to message with id 1218
637 640 #
638 641 # Links can refer other objects from other projects, using project identifier:
639 642 # identifier:r52
640 643 # identifier:document:"Some document"
641 644 # identifier:version:1.0.0
642 645 # identifier:source:some/file
643 646 def parse_redmine_links(text, project, obj, attr, only_path, options)
644 647 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|
645 648 leading, esc, project_prefix, project_identifier, prefix, sep, identifier = $1, $2, $3, $4, $5, $7 || $9, $8 || $10
646 649 link = nil
647 650 if project_identifier
648 651 project = Project.visible.find_by_identifier(project_identifier)
649 652 end
650 653 if esc.nil?
651 654 if prefix.nil? && sep == 'r'
652 655 # project.changesets.visible raises an SQL error because of a double join on repositories
653 656 if project && project.repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(project.repository.id, identifier))
654 657 link = link_to(h("#{project_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
655 658 :class => 'changeset',
656 659 :title => truncate_single_line(changeset.comments, :length => 100))
657 660 end
658 661 elsif sep == '#'
659 662 oid = identifier.to_i
660 663 case prefix
661 664 when nil
662 665 if issue = Issue.visible.find_by_id(oid, :include => :status)
663 666 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
664 667 :class => issue.css_classes,
665 668 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
666 669 end
667 670 when 'document'
668 671 if document = Document.visible.find_by_id(oid)
669 672 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
670 673 :class => 'document'
671 674 end
672 675 when 'version'
673 676 if version = Version.visible.find_by_id(oid)
674 677 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
675 678 :class => 'version'
676 679 end
677 680 when 'message'
678 681 if message = Message.visible.find_by_id(oid, :include => :parent)
679 682 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
680 683 end
681 684 when 'forum'
682 685 if board = Board.visible.find_by_id(oid)
683 686 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
684 687 :class => 'board'
685 688 end
686 689 when 'news'
687 690 if news = News.visible.find_by_id(oid)
688 691 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
689 692 :class => 'news'
690 693 end
691 694 when 'project'
692 695 if p = Project.visible.find_by_id(oid)
693 696 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
694 697 end
695 698 end
696 699 elsif sep == ':'
697 700 # removes the double quotes if any
698 701 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
699 702 case prefix
700 703 when 'document'
701 704 if project && document = project.documents.visible.find_by_title(name)
702 705 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
703 706 :class => 'document'
704 707 end
705 708 when 'version'
706 709 if project && version = project.versions.visible.find_by_name(name)
707 710 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
708 711 :class => 'version'
709 712 end
710 713 when 'forum'
711 714 if project && board = project.boards.visible.find_by_name(name)
712 715 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
713 716 :class => 'board'
714 717 end
715 718 when 'news'
716 719 if project && news = project.news.visible.find_by_title(name)
717 720 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
718 721 :class => 'news'
719 722 end
720 723 when 'commit'
721 724 if project && project.repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", project.repository.id, "#{name}%"]))
722 725 link = link_to h("#{project_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.identifier},
723 726 :class => 'changeset',
724 727 :title => truncate_single_line(h(changeset.comments), :length => 100)
725 728 end
726 729 when 'source', 'export'
727 730 if project && project.repository && User.current.allowed_to?(:browse_repository, project)
728 731 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
729 732 path, rev, anchor = $1, $3, $5
730 733 link = link_to h("#{project_prefix}#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
731 734 :path => to_path_param(path),
732 735 :rev => rev,
733 736 :anchor => anchor,
734 737 :format => (prefix == 'export' ? 'raw' : nil)},
735 738 :class => (prefix == 'export' ? 'source download' : 'source')
736 739 end
737 740 when 'attachment'
738 741 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
739 742 if attachments && attachment = attachments.detect {|a| a.filename == name }
740 743 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
741 744 :class => 'attachment'
742 745 end
743 746 when 'project'
744 747 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
745 748 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
746 749 end
747 750 end
748 751 end
749 752 end
750 753 (leading + (link || "#{project_prefix}#{prefix}#{sep}#{identifier}")).html_safe
751 754 end
752 755 end
753 756
754 757 HEADING_RE = /(<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>)/i unless const_defined?(:HEADING_RE)
755 758
756 759 def parse_sections(text, project, obj, attr, only_path, options)
757 760 return unless options[:edit_section_links]
758 761 text.gsub!(HEADING_RE) do
759 762 @current_section += 1
760 763 if @current_section > 1
761 764 content_tag('div',
762 765 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
763 766 :class => 'contextual',
764 767 :title => l(:button_edit_section)) + $1
765 768 else
766 769 $1
767 770 end
768 771 end
769 772 end
770 773
771 774 # Headings and TOC
772 775 # Adds ids and links to headings unless options[:headings] is set to false
773 776 def parse_headings(text, project, obj, attr, only_path, options)
774 777 return if options[:headings] == false
775 778
776 779 text.gsub!(HEADING_RE) do
777 780 level, attrs, content = $2.to_i, $3, $4
778 781 item = strip_tags(content).strip
779 782 anchor = sanitize_anchor_name(item)
780 783 # used for single-file wiki export
781 784 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
782 785 @heading_anchors[anchor] ||= 0
783 786 idx = (@heading_anchors[anchor] += 1)
784 787 if idx > 1
785 788 anchor = "#{anchor}-#{idx}"
786 789 end
787 790 @parsed_headings << [level, anchor, item]
788 791 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
789 792 end
790 793 end
791 794
792 795 MACROS_RE = /
793 796 (!)? # escaping
794 797 (
795 798 \{\{ # opening tag
796 799 ([\w]+) # macro name
797 800 (\(([^\}]*)\))? # optional arguments
798 801 \}\} # closing tag
799 802 )
800 803 /x unless const_defined?(:MACROS_RE)
801 804
802 805 # Macros substitution
803 806 def parse_macros(text, project, obj, attr, only_path, options)
804 807 text.gsub!(MACROS_RE) do
805 808 esc, all, macro = $1, $2, $3.downcase
806 809 args = ($5 || '').split(',').each(&:strip)
807 810 if esc.nil?
808 811 begin
809 812 exec_macro(macro, obj, args)
810 813 rescue => e
811 814 "<div class=\"flash error\">Error executing the <strong>#{macro}</strong> macro (#{e})</div>"
812 815 end || all
813 816 else
814 817 all
815 818 end
816 819 end
817 820 end
818 821
819 822 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
820 823
821 824 # Renders the TOC with given headings
822 825 def replace_toc(text, headings)
823 826 text.gsub!(TOC_RE) do
824 827 if headings.empty?
825 828 ''
826 829 else
827 830 div_class = 'toc'
828 831 div_class << ' right' if $1 == '>'
829 832 div_class << ' left' if $1 == '<'
830 833 out = "<ul class=\"#{div_class}\"><li>"
831 834 root = headings.map(&:first).min
832 835 current = root
833 836 started = false
834 837 headings.each do |level, anchor, item|
835 838 if level > current
836 839 out << '<ul><li>' * (level - current)
837 840 elsif level < current
838 841 out << "</li></ul>\n" * (current - level) + "</li><li>"
839 842 elsif started
840 843 out << '</li><li>'
841 844 end
842 845 out << "<a href=\"##{anchor}\">#{item}</a>"
843 846 current = level
844 847 started = true
845 848 end
846 849 out << '</li></ul>' * (current - root)
847 850 out << '</li></ul>'
848 851 end
849 852 end
850 853 end
851 854
852 855 # Same as Rails' simple_format helper without using paragraphs
853 856 def simple_format_without_paragraph(text)
854 857 text.to_s.
855 858 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
856 859 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
857 860 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
858 861 html_safe
859 862 end
860 863
861 864 def lang_options_for_select(blank=true)
862 865 (blank ? [["(auto)", ""]] : []) +
863 866 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
864 867 end
865 868
866 869 def label_tag_for(name, option_tags = nil, options = {})
867 870 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
868 871 content_tag("label", label_text)
869 872 end
870 873
871 874 def labelled_tabular_form_for(*args, &proc)
872 875 args << {} unless args.last.is_a?(Hash)
873 876 options = args.last
874 877 options[:html] ||= {}
875 878 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
876 879 options.merge!({:builder => TabularFormBuilder})
877 880 form_for(*args, &proc)
878 881 end
879 882
880 883 def labelled_form_for(*args, &proc)
881 884 args << {} unless args.last.is_a?(Hash)
882 885 options = args.last
883 886 options.merge!({:builder => TabularFormBuilder})
884 887 form_for(*args, &proc)
885 888 end
886 889
887 890 def back_url_hidden_field_tag
888 891 back_url = params[:back_url] || request.env['HTTP_REFERER']
889 892 back_url = CGI.unescape(back_url.to_s)
890 893 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
891 894 end
892 895
893 896 def check_all_links(form_name)
894 897 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
895 898 " | ".html_safe +
896 899 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
897 900 end
898 901
899 902 def progress_bar(pcts, options={})
900 903 pcts = [pcts, pcts] unless pcts.is_a?(Array)
901 904 pcts = pcts.collect(&:round)
902 905 pcts[1] = pcts[1] - pcts[0]
903 906 pcts << (100 - pcts[1] - pcts[0])
904 907 width = options[:width] || '100px;'
905 908 legend = options[:legend] || ''
906 909 content_tag('table',
907 910 content_tag('tr',
908 911 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
909 912 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
910 913 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
911 914 ), :class => 'progress', :style => "width: #{width};").html_safe +
912 915 content_tag('p', legend, :class => 'pourcent').html_safe
913 916 end
914 917
915 918 def checked_image(checked=true)
916 919 if checked
917 920 image_tag 'toggle_check.png'
918 921 end
919 922 end
920 923
921 924 def context_menu(url)
922 925 unless @context_menu_included
923 926 content_for :header_tags do
924 927 javascript_include_tag('context_menu') +
925 928 stylesheet_link_tag('context_menu')
926 929 end
927 930 if l(:direction) == 'rtl'
928 931 content_for :header_tags do
929 932 stylesheet_link_tag('context_menu_rtl')
930 933 end
931 934 end
932 935 @context_menu_included = true
933 936 end
934 937 javascript_tag "new ContextMenu('#{ url_for(url) }')"
935 938 end
936 939
937 940 def context_menu_link(name, url, options={})
938 941 options[:class] ||= ''
939 942 if options.delete(:selected)
940 943 options[:class] << ' icon-checked disabled'
941 944 options[:disabled] = true
942 945 end
943 946 if options.delete(:disabled)
944 947 options.delete(:method)
945 948 options.delete(:confirm)
946 949 options.delete(:onclick)
947 950 options[:class] << ' disabled'
948 951 url = '#'
949 952 end
950 953 link_to h(name), url, options
951 954 end
952 955
953 956 def calendar_for(field_id)
954 957 include_calendar_headers_tags
955 958 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
956 959 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
957 960 end
958 961
959 962 def include_calendar_headers_tags
960 963 unless @calendar_headers_tags_included
961 964 @calendar_headers_tags_included = true
962 965 content_for :header_tags do
963 966 start_of_week = case Setting.start_of_week.to_i
964 967 when 1
965 968 'Calendar._FD = 1;' # Monday
966 969 when 7
967 970 'Calendar._FD = 0;' # Sunday
968 971 when 6
969 972 'Calendar._FD = 6;' # Saturday
970 973 else
971 974 '' # use language
972 975 end
973 976
974 977 javascript_include_tag('calendar/calendar') +
975 978 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
976 979 javascript_tag(start_of_week) +
977 980 javascript_include_tag('calendar/calendar-setup') +
978 981 stylesheet_link_tag('calendar')
979 982 end
980 983 end
981 984 end
982 985
983 986 def content_for(name, content = nil, &block)
984 987 @has_content ||= {}
985 988 @has_content[name] = true
986 989 super(name, content, &block)
987 990 end
988 991
989 992 def has_content?(name)
990 993 (@has_content && @has_content[name]) || false
991 994 end
992 995
993 996 def email_delivery_enabled?
994 997 !!ActionMailer::Base.perform_deliveries
995 998 end
996 999
997 1000 # Returns the avatar image tag for the given +user+ if avatars are enabled
998 1001 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
999 1002 def avatar(user, options = { })
1000 1003 if Setting.gravatar_enabled?
1001 1004 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
1002 1005 email = nil
1003 1006 if user.respond_to?(:mail)
1004 1007 email = user.mail
1005 1008 elsif user.to_s =~ %r{<(.+?)>}
1006 1009 email = $1
1007 1010 end
1008 1011 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1009 1012 else
1010 1013 ''
1011 1014 end
1012 1015 end
1013 1016
1014 1017 def sanitize_anchor_name(anchor)
1015 1018 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1016 1019 end
1017 1020
1018 1021 # Returns the javascript tags that are included in the html layout head
1019 1022 def javascript_heads
1020 1023 tags = javascript_include_tag(:defaults)
1021 1024 unless User.current.pref.warn_on_leaving_unsaved == '0'
1022 1025 tags << "\n".html_safe + javascript_tag("Event.observe(window, 'load', function(){ new WarnLeavingUnsaved('#{escape_javascript( l(:text_warn_on_leaving_unsaved) )}'); });")
1023 1026 end
1024 1027 tags
1025 1028 end
1026 1029
1027 1030 def favicon
1028 1031 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1029 1032 end
1030 1033
1031 1034 def robot_exclusion_tag
1032 1035 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1033 1036 end
1034 1037
1035 1038 # Returns true if arg is expected in the API response
1036 1039 def include_in_api_response?(arg)
1037 1040 unless @included_in_api_response
1038 1041 param = params[:include]
1039 1042 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1040 1043 @included_in_api_response.collect!(&:strip)
1041 1044 end
1042 1045 @included_in_api_response.include?(arg.to_s)
1043 1046 end
1044 1047
1045 1048 # Returns options or nil if nometa param or X-Redmine-Nometa header
1046 1049 # was set in the request
1047 1050 def api_meta(options)
1048 1051 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1049 1052 # compatibility mode for activeresource clients that raise
1050 1053 # an error when unserializing an array with attributes
1051 1054 nil
1052 1055 else
1053 1056 options
1054 1057 end
1055 1058 end
1056 1059
1057 1060 private
1058 1061
1059 1062 def wiki_helper
1060 1063 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1061 1064 extend helper
1062 1065 return self
1063 1066 end
1064 1067
1065 1068 def link_to_content_update(text, url_params = {}, html_options = {})
1066 1069 link_to(text, url_params, html_options)
1067 1070 end
1068 1071 end
@@ -1,905 +1,950
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 h3. Subtitle with @inline code@
736
735 737 h1. Another title
736 738
737 739 h3. An "Internet link":http://www.redmine.org/ inside subtitle
738 740
739 741 h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
740 742
741 743 RAW
742 744
743 745 expected = '<ul class="toc">' +
744 746 '<li><a href="#Title">Title</a>' +
745 747 '<ul>' +
746 748 '<li><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
747 749 '<li><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
748 750 '<li><a href="#Subtitle-with-red-text">Subtitle with red text</a>' +
749 751 '<ul>' +
750 752 '<li><a href="#Subtitle-with-some-modifiers">Subtitle with some modifiers</a></li>' +
753 '<li><a href="#Subtitle-with-inline-code">Subtitle with inline code</a></li>' +
751 754 '</ul>' +
752 755 '</li>' +
753 756 '</ul>' +
754 757 '</li>' +
755 758 '<li><a href="#Another-title">Another title</a>' +
756 759 '<ul>' +
757 760 '<li>' +
758 761 '<ul>' +
759 762 '<li><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
760 763 '</ul>' +
761 764 '</li>' +
762 765 '<li><a href="#Project-Name">Project Name</a></li>' +
763 766 '</ul>' +
764 767 '</li>' +
765 768 '</ul>'
766 769
767 770 @project = Project.find(1)
768 771 assert textilizable(raw).gsub("\n", "").include?(expected)
769 772 end
770 773
771 774 def test_table_of_content_should_generate_unique_anchors
772 775 raw = <<-RAW
773 776 {{toc}}
774 777
775 778 h1. Title
776 779
777 780 h2. Subtitle
778 781
779 782 h2. Subtitle
780 783 RAW
781 784
782 785 expected = '<ul class="toc">' +
783 786 '<li><a href="#Title">Title</a>' +
784 787 '<ul>' +
785 788 '<li><a href="#Subtitle">Subtitle</a></li>' +
786 789 '<li><a href="#Subtitle-2">Subtitle</a></li>'
787 790 '</ul>'
788 791 '</li>' +
789 792 '</ul>'
790 793
791 794 @project = Project.find(1)
792 795 result = textilizable(raw).gsub("\n", "")
793 796 assert_include expected, result
794 797 assert_include '<a name="Subtitle">', result
795 798 assert_include '<a name="Subtitle-2">', result
796 799 end
797 800
798 801 def test_table_of_content_should_contain_included_page_headings
799 802 raw = <<-RAW
800 803 {{toc}}
801 804
802 805 h1. Included
803 806
804 807 {{include(Child_1)}}
805 808 RAW
806 809
807 810 expected = '<ul class="toc">' +
808 811 '<li><a href="#Included">Included</a></li>' +
809 812 '<li><a href="#Child-page-1">Child page 1</a></li>' +
810 813 '</ul>'
811 814
812 815 @project = Project.find(1)
813 816 assert textilizable(raw).gsub("\n", "").include?(expected)
814 817 end
815 818
819 def test_section_edit_links
820 raw = <<-RAW
821 h1. Title
822
823 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
824
825 h2. Subtitle with a [[Wiki]] link
826
827 h2. Subtitle with *some* _modifiers_
828
829 h2. Subtitle with @inline code@
830
831 <pre>
832 some code
833
834 h2. heading inside pre
835
836 <h2>html heading inside pre</h2>
837 </pre>
838
839 h2. Subtitle after pre tag
840 RAW
841
842 @project = Project.find(1)
843 set_language_if_valid 'en'
844 result = textilizable(raw, :edit_section_links => {:controller => 'wiki', :action => 'edit', :project_id => '1', :id => 'Test'}).gsub("\n", "")
845
846 # heading that contains inline code
847 assert_match Regexp.new('<div class="contextual" title="Edit this section">' +
848 '<a href="/projects/1/wiki/Test/edit\?section=4"><img alt="Edit" src="/images/edit.png(\?\d+)?" /></a></div>' +
849 '<a name="Subtitle-with-inline-code"></a>' +
850 '<h2 >Subtitle with <code>inline code</code><a href="#Subtitle-with-inline-code" class="wiki-anchor">&para;</a></h2>'),
851 result
852
853 # last heading
854 assert_match Regexp.new('<div class="contextual" title="Edit this section">' +
855 '<a href="/projects/1/wiki/Test/edit\?section=5"><img alt="Edit" src="/images/edit.png(\?\d+)?" /></a></div>' +
856 '<a name="Subtitle-after-pre-tag"></a>' +
857 '<h2 >Subtitle after pre tag<a href="#Subtitle-after-pre-tag" class="wiki-anchor">&para;</a></h2>'),
858 result
859 end
860
816 861 def test_default_formatter
817 862 Setting.text_formatting = 'unknown'
818 863 text = 'a *link*: http://www.example.net/'
819 864 assert_equal '<p>a *link*: <a href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
820 865 Setting.text_formatting = 'textile'
821 866 end
822 867
823 868 def test_due_date_distance_in_words
824 869 to_test = { Date.today => 'Due in 0 days',
825 870 Date.today + 1 => 'Due in 1 day',
826 871 Date.today + 100 => 'Due in about 3 months',
827 872 Date.today + 20000 => 'Due in over 54 years',
828 873 Date.today - 1 => '1 day late',
829 874 Date.today - 100 => 'about 3 months late',
830 875 Date.today - 20000 => 'over 54 years late',
831 876 }
832 877 ::I18n.locale = :en
833 878 to_test.each do |date, expected|
834 879 assert_equal expected, due_date_distance_in_words(date)
835 880 end
836 881 end
837 882
838 883 def test_avatar
839 884 # turn on avatars
840 885 Setting.gravatar_enabled = '1'
841 886 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
842 887 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
843 888 assert_nil avatar('jsmith')
844 889 assert_nil avatar(nil)
845 890
846 891 # turn off avatars
847 892 Setting.gravatar_enabled = '0'
848 893 assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
849 894 end
850 895
851 896 def test_link_to_user
852 897 user = User.find(2)
853 898 t = link_to_user(user)
854 899 assert_equal "<a href=\"/users/2\">#{ user.name }</a>", t
855 900 end
856 901
857 902 def test_link_to_user_should_not_link_to_locked_user
858 903 user = User.find(5)
859 904 assert user.locked?
860 905 t = link_to_user(user)
861 906 assert_equal user.name, t
862 907 end
863 908
864 909 def test_link_to_user_should_not_link_to_anonymous
865 910 user = User.anonymous
866 911 assert user.anonymous?
867 912 t = link_to_user(user)
868 913 assert_equal ::I18n.t(:label_user_anonymous), t
869 914 end
870 915
871 916 def test_link_to_project
872 917 project = Project.find(1)
873 918 assert_equal %(<a href="/projects/ecookbook">eCookbook</a>),
874 919 link_to_project(project)
875 920 assert_equal %(<a href="/projects/ecookbook/settings">eCookbook</a>),
876 921 link_to_project(project, :action => 'settings')
877 922 assert_equal %(<a href="http://test.host/projects/ecookbook?jump=blah">eCookbook</a>),
878 923 link_to_project(project, {:only_path => false, :jump => 'blah'})
879 924 assert_equal %(<a href="/projects/ecookbook/settings" class="project">eCookbook</a>),
880 925 link_to_project(project, {:action => 'settings'}, :class => "project")
881 926 end
882 927
883 928 def test_principals_options_for_select_with_users
884 929 users = [User.find(2), User.find(4)]
885 930 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>),
886 931 principals_options_for_select(users)
887 932 end
888 933
889 934 def test_principals_options_for_select_with_selected
890 935 users = [User.find(2), User.find(4)]
891 936 assert_equal %(<option value="2">John Smith</option><option value="4" selected="selected">Robert Hill</option>),
892 937 principals_options_for_select(users, User.find(4))
893 938 end
894 939
895 940 def test_principals_options_for_select_with_users_and_groups
896 941 users = [User.find(2), Group.find(11), User.find(4), Group.find(10)]
897 942 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>) +
898 943 %(<optgroup label="Groups"><option value="10">A Team</option><option value="11">B Team</option></optgroup>),
899 944 principals_options_for_select(users)
900 945 end
901 946
902 947 def test_principals_options_for_select_with_empty_collection
903 948 assert_equal '', principals_options_for_select([])
904 949 end
905 950 end
General Comments 0
You need to be logged in to leave comments. Login now