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