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