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