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