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