##// END OF EJS Templates
Merged r4257 from trunk....
Eric Davis -
r4207:79a0d9242180
parent child
Show More
@@ -1,857 +1,843
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'forwardable'
19 19 require 'cgi'
20 20
21 21 module ApplicationHelper
22 22 include Redmine::WikiFormatting::Macros::Definitions
23 23 include Redmine::I18n
24 24 include GravatarHelper::PublicMethods
25 25
26 26 extend Forwardable
27 27 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
28 28
29 29 # Return true if user is authorized for controller/action, otherwise false
30 30 def authorize_for(controller, action)
31 31 User.current.allowed_to?({:controller => controller, :action => action}, @project)
32 32 end
33 33
34 34 # Display a link if user is authorized
35 35 #
36 36 # @param [String] name Anchor text (passed to link_to)
37 # @param [Hash, String] options Hash params or url for the link target (passed to link_to).
38 # This will checked by authorize_for to see if the user is authorized
37 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
39 38 # @param [optional, Hash] html_options Options passed to link_to
40 39 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
41 40 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
42 if options.is_a?(String)
43 begin
44 route = ActionController::Routing::Routes.recognize_path(options.gsub(/\?.*/,''), :method => options[:method] || :get)
45 link_controller = route[:controller]
46 link_action = route[:action]
47 rescue ActionController::RoutingError # Parse failed, not a route
48 link_controller, link_action = nil, nil
49 end
50 else
51 link_controller = options[:controller] || params[:controller]
52 link_action = options[:action]
53 end
54
55 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(link_controller, link_action)
41 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
56 42 end
57 43
58 44 # Display a link to remote if user is authorized
59 45 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
60 46 url = options[:url] || {}
61 47 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
62 48 end
63 49
64 50 # Displays a link to user's account page if active
65 51 def link_to_user(user, options={})
66 52 if user.is_a?(User)
67 53 name = h(user.name(options[:format]))
68 54 if user.active?
69 55 link_to name, :controller => 'users', :action => 'show', :id => user
70 56 else
71 57 name
72 58 end
73 59 else
74 60 h(user.to_s)
75 61 end
76 62 end
77 63
78 64 # Displays a link to +issue+ with its subject.
79 65 # Examples:
80 66 #
81 67 # link_to_issue(issue) # => Defect #6: This is the subject
82 68 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
83 69 # link_to_issue(issue, :subject => false) # => Defect #6
84 70 # link_to_issue(issue, :project => true) # => Foo - Defect #6
85 71 #
86 72 def link_to_issue(issue, options={})
87 73 title = nil
88 74 subject = nil
89 75 if options[:subject] == false
90 76 title = truncate(issue.subject, :length => 60)
91 77 else
92 78 subject = issue.subject
93 79 if options[:truncate]
94 80 subject = truncate(subject, :length => options[:truncate])
95 81 end
96 82 end
97 83 s = link_to "#{issue.tracker} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
98 84 :class => issue.css_classes,
99 85 :title => title
100 86 s << ": #{h subject}" if subject
101 87 s = "#{h issue.project} - " + s if options[:project]
102 88 s
103 89 end
104 90
105 91 # Generates a link to an attachment.
106 92 # Options:
107 93 # * :text - Link text (default to attachment filename)
108 94 # * :download - Force download (default: false)
109 95 def link_to_attachment(attachment, options={})
110 96 text = options.delete(:text) || attachment.filename
111 97 action = options.delete(:download) ? 'download' : 'show'
112 98
113 99 link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
114 100 end
115 101
116 102 # Generates a link to a SCM revision
117 103 # Options:
118 104 # * :text - Link text (default to the formatted revision)
119 105 def link_to_revision(revision, project, options={})
120 106 text = options.delete(:text) || format_revision(revision)
121 107
122 108 link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => revision}, :title => l(:label_revision_id, revision))
123 109 end
124 110
125 111 # Generates a link to a project if active
126 112 # Examples:
127 113 #
128 114 # link_to_project(project) # => link to the specified project overview
129 115 # link_to_project(project, :action=>'settings') # => link to project settings
130 116 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
131 117 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
132 118 #
133 119 def link_to_project(project, options={}, html_options = nil)
134 120 if project.active?
135 121 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
136 122 link_to(h(project), url, html_options)
137 123 else
138 124 h(project)
139 125 end
140 126 end
141 127
142 128 def toggle_link(name, id, options={})
143 129 onclick = "Element.toggle('#{id}'); "
144 130 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
145 131 onclick << "return false;"
146 132 link_to(name, "#", :onclick => onclick)
147 133 end
148 134
149 135 def image_to_function(name, function, html_options = {})
150 136 html_options.symbolize_keys!
151 137 tag(:input, html_options.merge({
152 138 :type => "image", :src => image_path(name),
153 139 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
154 140 }))
155 141 end
156 142
157 143 def prompt_to_remote(name, text, param, url, html_options = {})
158 144 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
159 145 link_to name, {}, html_options
160 146 end
161 147
162 148 def format_activity_title(text)
163 149 h(truncate_single_line(text, :length => 100))
164 150 end
165 151
166 152 def format_activity_day(date)
167 153 date == Date.today ? l(:label_today).titleize : format_date(date)
168 154 end
169 155
170 156 def format_activity_description(text)
171 157 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
172 158 end
173 159
174 160 def format_version_name(version)
175 161 if version.project == @project
176 162 h(version)
177 163 else
178 164 h("#{version.project} - #{version}")
179 165 end
180 166 end
181 167
182 168 def due_date_distance_in_words(date)
183 169 if date
184 170 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
185 171 end
186 172 end
187 173
188 174 def render_page_hierarchy(pages, node=nil)
189 175 content = ''
190 176 if pages[node]
191 177 content << "<ul class=\"pages-hierarchy\">\n"
192 178 pages[node].each do |page|
193 179 content << "<li>"
194 180 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'index', :id => page.project, :page => page.title},
195 181 :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
196 182 content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
197 183 content << "</li>\n"
198 184 end
199 185 content << "</ul>\n"
200 186 end
201 187 content
202 188 end
203 189
204 190 # Renders flash messages
205 191 def render_flash_messages
206 192 s = ''
207 193 flash.each do |k,v|
208 194 s << content_tag('div', v, :class => "flash #{k}")
209 195 end
210 196 s
211 197 end
212 198
213 199 # Renders tabs and their content
214 200 def render_tabs(tabs)
215 201 if tabs.any?
216 202 render :partial => 'common/tabs', :locals => {:tabs => tabs}
217 203 else
218 204 content_tag 'p', l(:label_no_data), :class => "nodata"
219 205 end
220 206 end
221 207
222 208 # Renders the project quick-jump box
223 209 def render_project_jump_box
224 210 # Retrieve them now to avoid a COUNT query
225 211 projects = User.current.projects.all
226 212 if projects.any?
227 213 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
228 214 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
229 215 '<option value="" disabled="disabled">---</option>'
230 216 s << project_tree_options_for_select(projects, :selected => @project) do |p|
231 217 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
232 218 end
233 219 s << '</select>'
234 220 s
235 221 end
236 222 end
237 223
238 224 def project_tree_options_for_select(projects, options = {})
239 225 s = ''
240 226 project_tree(projects) do |project, level|
241 227 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
242 228 tag_options = {:value => project.id}
243 229 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
244 230 tag_options[:selected] = 'selected'
245 231 else
246 232 tag_options[:selected] = nil
247 233 end
248 234 tag_options.merge!(yield(project)) if block_given?
249 235 s << content_tag('option', name_prefix + h(project), tag_options)
250 236 end
251 237 s
252 238 end
253 239
254 240 # Yields the given block for each project with its level in the tree
255 241 def project_tree(projects, &block)
256 242 ancestors = []
257 243 projects.sort_by(&:lft).each do |project|
258 244 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
259 245 ancestors.pop
260 246 end
261 247 yield project, ancestors.size
262 248 ancestors << project
263 249 end
264 250 end
265 251
266 252 def project_nested_ul(projects, &block)
267 253 s = ''
268 254 if projects.any?
269 255 ancestors = []
270 256 projects.sort_by(&:lft).each do |project|
271 257 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
272 258 s << "<ul>\n"
273 259 else
274 260 ancestors.pop
275 261 s << "</li>"
276 262 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
277 263 ancestors.pop
278 264 s << "</ul></li>\n"
279 265 end
280 266 end
281 267 s << "<li>"
282 268 s << yield(project).to_s
283 269 ancestors << project
284 270 end
285 271 s << ("</li></ul>\n" * ancestors.size)
286 272 end
287 273 s
288 274 end
289 275
290 276 def principals_check_box_tags(name, principals)
291 277 s = ''
292 278 principals.sort.each do |principal|
293 279 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
294 280 end
295 281 s
296 282 end
297 283
298 284 # Truncates and returns the string as a single line
299 285 def truncate_single_line(string, *args)
300 286 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
301 287 end
302 288
303 289 # Truncates at line break after 250 characters or options[:length]
304 290 def truncate_lines(string, options={})
305 291 length = options[:length] || 250
306 292 if string.to_s =~ /\A(.{#{length}}.*?)$/m
307 293 "#{$1}..."
308 294 else
309 295 string
310 296 end
311 297 end
312 298
313 299 def html_hours(text)
314 300 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
315 301 end
316 302
317 303 def authoring(created, author, options={})
318 304 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created))
319 305 end
320 306
321 307 def time_tag(time)
322 308 text = distance_of_time_in_words(Time.now, time)
323 309 if @project
324 310 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
325 311 else
326 312 content_tag('acronym', text, :title => format_time(time))
327 313 end
328 314 end
329 315
330 316 def syntax_highlight(name, content)
331 317 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
332 318 end
333 319
334 320 def to_path_param(path)
335 321 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
336 322 end
337 323
338 324 def pagination_links_full(paginator, count=nil, options={})
339 325 page_param = options.delete(:page_param) || :page
340 326 per_page_links = options.delete(:per_page_links)
341 327 url_param = params.dup
342 328 # don't reuse query params if filters are present
343 329 url_param.merge!(:fields => nil, :values => nil, :operators => nil) if url_param.delete(:set_filter)
344 330
345 331 html = ''
346 332 if paginator.current.previous
347 333 html << link_to_remote_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
348 334 end
349 335
350 336 html << (pagination_links_each(paginator, options) do |n|
351 337 link_to_remote_content_update(n.to_s, url_param.merge(page_param => n))
352 338 end || '')
353 339
354 340 if paginator.current.next
355 341 html << ' ' + link_to_remote_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next))
356 342 end
357 343
358 344 unless count.nil?
359 345 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
360 346 if per_page_links != false && links = per_page_links(paginator.items_per_page)
361 347 html << " | #{links}"
362 348 end
363 349 end
364 350
365 351 html
366 352 end
367 353
368 354 def per_page_links(selected=nil)
369 355 url_param = params.dup
370 356 url_param.clear if url_param.has_key?(:set_filter)
371 357
372 358 links = Setting.per_page_options_array.collect do |n|
373 359 n == selected ? n : link_to_remote(n, {:update => "content",
374 360 :url => params.dup.merge(:per_page => n),
375 361 :method => :get},
376 362 {:href => url_for(url_param.merge(:per_page => n))})
377 363 end
378 364 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
379 365 end
380 366
381 367 def reorder_links(name, url)
382 368 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), url.merge({"#{name}[move_to]" => 'highest'}), :method => :post, :title => l(:label_sort_highest)) +
383 369 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), url.merge({"#{name}[move_to]" => 'higher'}), :method => :post, :title => l(:label_sort_higher)) +
384 370 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), url.merge({"#{name}[move_to]" => 'lower'}), :method => :post, :title => l(:label_sort_lower)) +
385 371 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), url.merge({"#{name}[move_to]" => 'lowest'}), :method => :post, :title => l(:label_sort_lowest))
386 372 end
387 373
388 374 def breadcrumb(*args)
389 375 elements = args.flatten
390 376 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
391 377 end
392 378
393 379 def other_formats_links(&block)
394 380 concat('<p class="other-formats">' + l(:label_export_to))
395 381 yield Redmine::Views::OtherFormatsBuilder.new(self)
396 382 concat('</p>')
397 383 end
398 384
399 385 def page_header_title
400 386 if @project.nil? || @project.new_record?
401 387 h(Setting.app_title)
402 388 else
403 389 b = []
404 390 ancestors = (@project.root? ? [] : @project.ancestors.visible)
405 391 if ancestors.any?
406 392 root = ancestors.shift
407 393 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
408 394 if ancestors.size > 2
409 395 b << '&#8230;'
410 396 ancestors = ancestors[-2, 2]
411 397 end
412 398 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
413 399 end
414 400 b << h(@project)
415 401 b.join(' &#187; ')
416 402 end
417 403 end
418 404
419 405 def html_title(*args)
420 406 if args.empty?
421 407 title = []
422 408 title << @project.name if @project
423 409 title += @html_title if @html_title
424 410 title << Setting.app_title
425 411 title.select {|t| !t.blank? }.join(' - ')
426 412 else
427 413 @html_title ||= []
428 414 @html_title += args
429 415 end
430 416 end
431 417
432 418 # Returns the theme, controller name, and action as css classes for the
433 419 # HTML body.
434 420 def body_css_classes
435 421 css = []
436 422 if theme = Redmine::Themes.theme(Setting.ui_theme)
437 423 css << 'theme-' + theme.name
438 424 end
439 425
440 426 css << 'controller-' + params[:controller]
441 427 css << 'action-' + params[:action]
442 428 css.join(' ')
443 429 end
444 430
445 431 def accesskey(s)
446 432 Redmine::AccessKeys.key_for s
447 433 end
448 434
449 435 # Formats text according to system settings.
450 436 # 2 ways to call this method:
451 437 # * with a String: textilizable(text, options)
452 438 # * with an object and one of its attribute: textilizable(issue, :description, options)
453 439 def textilizable(*args)
454 440 options = args.last.is_a?(Hash) ? args.pop : {}
455 441 case args.size
456 442 when 1
457 443 obj = options[:object]
458 444 text = args.shift
459 445 when 2
460 446 obj = args.shift
461 447 attr = args.shift
462 448 text = obj.send(attr).to_s
463 449 else
464 450 raise ArgumentError, 'invalid arguments to textilizable'
465 451 end
466 452 return '' if text.blank?
467 453 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
468 454 only_path = options.delete(:only_path) == false ? false : true
469 455
470 456 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }
471 457
472 458 parse_non_pre_blocks(text) do |text|
473 459 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
474 460 send method_name, text, project, obj, attr, only_path, options
475 461 end
476 462 end
477 463 end
478 464
479 465 def parse_non_pre_blocks(text)
480 466 s = StringScanner.new(text)
481 467 tags = []
482 468 parsed = ''
483 469 while !s.eos?
484 470 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
485 471 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
486 472 if tags.empty?
487 473 yield text
488 474 end
489 475 parsed << text
490 476 if tag
491 477 if closing
492 478 if tags.last == tag.downcase
493 479 tags.pop
494 480 end
495 481 else
496 482 tags << tag.downcase
497 483 end
498 484 parsed << full_tag
499 485 end
500 486 end
501 487 # Close any non closing tags
502 488 while tag = tags.pop
503 489 parsed << "</#{tag}>"
504 490 end
505 491 parsed
506 492 end
507 493
508 494 def parse_inline_attachments(text, project, obj, attr, only_path, options)
509 495 # when using an image link, try to use an attachment, if possible
510 496 if options[:attachments] || (obj && obj.respond_to?(:attachments))
511 497 attachments = nil
512 498 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
513 499 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
514 500 attachments ||= (options[:attachments] || obj.attachments).sort_by(&:created_on).reverse
515 501 # search for the picture in attachments
516 502 if found = attachments.detect { |att| att.filename.downcase == filename }
517 503 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
518 504 desc = found.description.to_s.gsub('"', '')
519 505 if !desc.blank? && alttext.blank?
520 506 alt = " title=\"#{desc}\" alt=\"#{desc}\""
521 507 end
522 508 "src=\"#{image_url}\"#{alt}"
523 509 else
524 510 m
525 511 end
526 512 end
527 513 end
528 514 end
529 515
530 516 # Wiki links
531 517 #
532 518 # Examples:
533 519 # [[mypage]]
534 520 # [[mypage|mytext]]
535 521 # wiki links can refer other project wikis, using project name or identifier:
536 522 # [[project:]] -> wiki starting page
537 523 # [[project:|mytext]]
538 524 # [[project:mypage]]
539 525 # [[project:mypage|mytext]]
540 526 def parse_wiki_links(text, project, obj, attr, only_path, options)
541 527 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
542 528 link_project = project
543 529 esc, all, page, title = $1, $2, $3, $5
544 530 if esc.nil?
545 531 if page =~ /^([^\:]+)\:(.*)$/
546 532 link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
547 533 page = $2
548 534 title ||= $1 if page.blank?
549 535 end
550 536
551 537 if link_project && link_project.wiki
552 538 # extract anchor
553 539 anchor = nil
554 540 if page =~ /^(.+?)\#(.+)$/
555 541 page, anchor = $1, $2
556 542 end
557 543 # check if page exists
558 544 wiki_page = link_project.wiki.find_page(page)
559 545 url = case options[:wiki_links]
560 546 when :local; "#{title}.html"
561 547 when :anchor; "##{title}" # used for single-file wiki export
562 548 else
563 549 url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => link_project, :page => Wiki.titleize(page), :anchor => anchor)
564 550 end
565 551 link_to((title || page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
566 552 else
567 553 # project or wiki doesn't exist
568 554 all
569 555 end
570 556 else
571 557 all
572 558 end
573 559 end
574 560 end
575 561
576 562 # Redmine links
577 563 #
578 564 # Examples:
579 565 # Issues:
580 566 # #52 -> Link to issue #52
581 567 # Changesets:
582 568 # r52 -> Link to revision 52
583 569 # commit:a85130f -> Link to scmid starting with a85130f
584 570 # Documents:
585 571 # document#17 -> Link to document with id 17
586 572 # document:Greetings -> Link to the document with title "Greetings"
587 573 # document:"Some document" -> Link to the document with title "Some document"
588 574 # Versions:
589 575 # version#3 -> Link to version with id 3
590 576 # version:1.0.0 -> Link to version named "1.0.0"
591 577 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
592 578 # Attachments:
593 579 # attachment:file.zip -> Link to the attachment of the current object named file.zip
594 580 # Source files:
595 581 # source:some/file -> Link to the file located at /some/file in the project's repository
596 582 # source:some/file@52 -> Link to the file's revision 52
597 583 # source:some/file#L120 -> Link to line 120 of the file
598 584 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
599 585 # export:some/file -> Force the download of the file
600 586 # Forum messages:
601 587 # message#1218 -> Link to message with id 1218
602 588 def parse_redmine_links(text, project, obj, attr, only_path, options)
603 589 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(attachment|document|version|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m|
604 590 leading, esc, prefix, sep, identifier = $1, $2, $3, $5 || $7, $6 || $8
605 591 link = nil
606 592 if esc.nil?
607 593 if prefix.nil? && sep == 'r'
608 594 if project && (changeset = project.changesets.find_by_revision(identifier))
609 595 link = link_to("r#{identifier}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
610 596 :class => 'changeset',
611 597 :title => truncate_single_line(changeset.comments, :length => 100))
612 598 end
613 599 elsif sep == '#'
614 600 oid = identifier.to_i
615 601 case prefix
616 602 when nil
617 603 if issue = Issue.visible.find_by_id(oid, :include => :status)
618 604 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
619 605 :class => issue.css_classes,
620 606 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
621 607 end
622 608 when 'document'
623 609 if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
624 610 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
625 611 :class => 'document'
626 612 end
627 613 when 'version'
628 614 if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
629 615 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
630 616 :class => 'version'
631 617 end
632 618 when 'message'
633 619 if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
634 620 link = link_to h(truncate(message.subject, :length => 60)), {:only_path => only_path,
635 621 :controller => 'messages',
636 622 :action => 'show',
637 623 :board_id => message.board,
638 624 :id => message.root,
639 625 :anchor => (message.parent ? "message-#{message.id}" : nil)},
640 626 :class => 'message'
641 627 end
642 628 when 'project'
643 629 if p = Project.visible.find_by_id(oid)
644 630 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
645 631 end
646 632 end
647 633 elsif sep == ':'
648 634 # removes the double quotes if any
649 635 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
650 636 case prefix
651 637 when 'document'
652 638 if project && document = project.documents.find_by_title(name)
653 639 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
654 640 :class => 'document'
655 641 end
656 642 when 'version'
657 643 if project && version = project.versions.find_by_name(name)
658 644 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
659 645 :class => 'version'
660 646 end
661 647 when 'commit'
662 648 if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
663 649 link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
664 650 :class => 'changeset',
665 651 :title => truncate_single_line(changeset.comments, :length => 100)
666 652 end
667 653 when 'source', 'export'
668 654 if project && project.repository
669 655 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
670 656 path, rev, anchor = $1, $3, $5
671 657 link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
672 658 :path => to_path_param(path),
673 659 :rev => rev,
674 660 :anchor => anchor,
675 661 :format => (prefix == 'export' ? 'raw' : nil)},
676 662 :class => (prefix == 'export' ? 'source download' : 'source')
677 663 end
678 664 when 'attachment'
679 665 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
680 666 if attachments && attachment = attachments.detect {|a| a.filename == name }
681 667 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
682 668 :class => 'attachment'
683 669 end
684 670 when 'project'
685 671 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
686 672 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
687 673 end
688 674 end
689 675 end
690 676 end
691 677 leading + (link || "#{prefix}#{sep}#{identifier}")
692 678 end
693 679 end
694 680
695 681 # Same as Rails' simple_format helper without using paragraphs
696 682 def simple_format_without_paragraph(text)
697 683 text.to_s.
698 684 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
699 685 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
700 686 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
701 687 end
702 688
703 689 def lang_options_for_select(blank=true)
704 690 (blank ? [["(auto)", ""]] : []) +
705 691 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
706 692 end
707 693
708 694 def label_tag_for(name, option_tags = nil, options = {})
709 695 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
710 696 content_tag("label", label_text)
711 697 end
712 698
713 699 def labelled_tabular_form_for(name, object, options, &proc)
714 700 options[:html] ||= {}
715 701 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
716 702 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
717 703 end
718 704
719 705 def back_url_hidden_field_tag
720 706 back_url = params[:back_url] || request.env['HTTP_REFERER']
721 707 back_url = CGI.unescape(back_url.to_s)
722 708 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
723 709 end
724 710
725 711 def check_all_links(form_name)
726 712 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
727 713 " | " +
728 714 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
729 715 end
730 716
731 717 def progress_bar(pcts, options={})
732 718 pcts = [pcts, pcts] unless pcts.is_a?(Array)
733 719 pcts = pcts.collect(&:round)
734 720 pcts[1] = pcts[1] - pcts[0]
735 721 pcts << (100 - pcts[1] - pcts[0])
736 722 width = options[:width] || '100px;'
737 723 legend = options[:legend] || ''
738 724 content_tag('table',
739 725 content_tag('tr',
740 726 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : '') +
741 727 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : '') +
742 728 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : '')
743 729 ), :class => 'progress', :style => "width: #{width};") +
744 730 content_tag('p', legend, :class => 'pourcent')
745 731 end
746 732
747 733 def checked_image(checked=true)
748 734 if checked
749 735 image_tag 'toggle_check.png'
750 736 end
751 737 end
752 738
753 739 def context_menu(url)
754 740 unless @context_menu_included
755 741 content_for :header_tags do
756 742 javascript_include_tag('context_menu') +
757 743 stylesheet_link_tag('context_menu')
758 744 end
759 745 if l(:direction) == 'rtl'
760 746 content_for :header_tags do
761 747 stylesheet_link_tag('context_menu_rtl')
762 748 end
763 749 end
764 750 @context_menu_included = true
765 751 end
766 752 javascript_tag "new ContextMenu('#{ url_for(url) }')"
767 753 end
768 754
769 755 def context_menu_link(name, url, options={})
770 756 options[:class] ||= ''
771 757 if options.delete(:selected)
772 758 options[:class] << ' icon-checked disabled'
773 759 options[:disabled] = true
774 760 end
775 761 if options.delete(:disabled)
776 762 options.delete(:method)
777 763 options.delete(:confirm)
778 764 options.delete(:onclick)
779 765 options[:class] << ' disabled'
780 766 url = '#'
781 767 end
782 768 link_to name, url, options
783 769 end
784 770
785 771 def calendar_for(field_id)
786 772 include_calendar_headers_tags
787 773 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
788 774 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
789 775 end
790 776
791 777 def include_calendar_headers_tags
792 778 unless @calendar_headers_tags_included
793 779 @calendar_headers_tags_included = true
794 780 content_for :header_tags do
795 781 start_of_week = case Setting.start_of_week.to_i
796 782 when 1
797 783 'Calendar._FD = 1;' # Monday
798 784 when 7
799 785 'Calendar._FD = 0;' # Sunday
800 786 else
801 787 '' # use language
802 788 end
803 789
804 790 javascript_include_tag('calendar/calendar') +
805 791 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
806 792 javascript_tag(start_of_week) +
807 793 javascript_include_tag('calendar/calendar-setup') +
808 794 stylesheet_link_tag('calendar')
809 795 end
810 796 end
811 797 end
812 798
813 799 def content_for(name, content = nil, &block)
814 800 @has_content ||= {}
815 801 @has_content[name] = true
816 802 super(name, content, &block)
817 803 end
818 804
819 805 def has_content?(name)
820 806 (@has_content && @has_content[name]) || false
821 807 end
822 808
823 809 # Returns the avatar image tag for the given +user+ if avatars are enabled
824 810 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
825 811 def avatar(user, options = { })
826 812 if Setting.gravatar_enabled?
827 813 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
828 814 email = nil
829 815 if user.respond_to?(:mail)
830 816 email = user.mail
831 817 elsif user.to_s =~ %r{<(.+?)>}
832 818 email = $1
833 819 end
834 820 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
835 821 end
836 822 end
837 823
838 824 def favicon
839 825 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />"
840 826 end
841 827
842 828 private
843 829
844 830 def wiki_helper
845 831 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
846 832 extend helper
847 833 return self
848 834 end
849 835
850 836 def link_to_remote_content_update(text, url_params)
851 837 link_to_remote(text,
852 838 {:url => url_params, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'},
853 839 {:href => url_for(:params => url_params)}
854 840 )
855 841 end
856 842
857 843 end
@@ -1,638 +1,629
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2009 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../../test_helper'
19 19
20 20 class ApplicationHelperTest < ActionView::TestCase
21 21
22 22 fixtures :projects, :roles, :enabled_modules, :users,
23 23 :repositories, :changesets,
24 24 :trackers, :issue_statuses, :issues, :versions, :documents,
25 25 :wikis, :wiki_pages, :wiki_contents,
26 26 :boards, :messages,
27 27 :attachments,
28 28 :enumerations
29 29
30 30 def setup
31 31 super
32 32 end
33 33
34 34 context "#link_to_if_authorized" do
35 35 context "authorized user" do
36 36 should "be tested"
37 37 end
38 38
39 39 context "unauthorized user" do
40 40 should "be tested"
41 41 end
42 42
43 43 should "allow using the :controller and :action for the target link" do
44 44 User.current = User.find_by_login('admin')
45 45
46 46 @project = Issue.first.project # Used by helper
47 47 response = link_to_if_authorized("By controller/action",
48 48 {:controller => 'issues', :action => 'edit', :id => Issue.first.id})
49 49 assert_match /href/, response
50 50 end
51 51
52 should "allow using the url for the target link" do
53 User.current = User.find_by_login('admin')
54
55 @project = Issue.first.project # Used by helper
56 response = link_to_if_authorized("By url",
57 new_issue_move_path(:id => Issue.first.id))
58 assert_match /href/, response
59 end
60
61 52 end
62 53
63 54 def test_auto_links
64 55 to_test = {
65 56 'http://foo.bar' => '<a class="external" href="http://foo.bar">http://foo.bar</a>',
66 57 'http://foo.bar/~user' => '<a class="external" href="http://foo.bar/~user">http://foo.bar/~user</a>',
67 58 'http://foo.bar.' => '<a class="external" href="http://foo.bar">http://foo.bar</a>.',
68 59 'https://foo.bar.' => '<a class="external" href="https://foo.bar">https://foo.bar</a>.',
69 60 'This is a link: http://foo.bar.' => 'This is a link: <a class="external" href="http://foo.bar">http://foo.bar</a>.',
70 61 'A link (eg. http://foo.bar).' => 'A link (eg. <a class="external" href="http://foo.bar">http://foo.bar</a>).',
71 62 '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>.',
72 63 'http://www.foo.bar/Test_(foobar)' => '<a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>',
73 64 '(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>)',
74 65 '(see inline link : http://www.foo.bar/Test)' => '(see inline link : <a class="external" href="http://www.foo.bar/Test">http://www.foo.bar/Test</a>)',
75 66 '(see inline link : http://www.foo.bar/Test).' => '(see inline link : <a class="external" href="http://www.foo.bar/Test">http://www.foo.bar/Test</a>).',
76 67 '(see "inline link":http://www.foo.bar/Test_(foobar))' => '(see <a href="http://www.foo.bar/Test_(foobar)" class="external">inline link</a>)',
77 68 '(see "inline link":http://www.foo.bar/Test)' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>)',
78 69 '(see "inline link":http://www.foo.bar/Test).' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>).',
79 70 'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>',
80 71 '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>',
81 72 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>',
82 73 'http://foo@www.bar.com' => '<a class="external" href="http://foo@www.bar.com">http://foo@www.bar.com</a>',
83 74 'http://foo:bar@www.bar.com' => '<a class="external" href="http://foo:bar@www.bar.com">http://foo:bar@www.bar.com</a>',
84 75 'ftp://foo.bar' => '<a class="external" href="ftp://foo.bar">ftp://foo.bar</a>',
85 76 'ftps://foo.bar' => '<a class="external" href="ftps://foo.bar">ftps://foo.bar</a>',
86 77 'sftp://foo.bar' => '<a class="external" href="sftp://foo.bar">sftp://foo.bar</a>',
87 78 # two exclamation marks
88 79 '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>',
89 80 # escaping
90 81 'http://foo"bar' => '<a class="external" href="http://foo&quot;bar">http://foo"bar</a>',
91 82 }
92 83 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
93 84 end
94 85
95 86 def test_auto_mailto
96 87 assert_equal '<p><a class="email" href="mailto:test@foo.bar">test@foo.bar</a></p>',
97 88 textilizable('test@foo.bar')
98 89 end
99 90
100 91 def test_inline_images
101 92 to_test = {
102 93 '!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
103 94 'floating !>http://foo.bar/image.jpg!' => 'floating <div style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></div>',
104 95 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class <img src="http://foo.bar/image.jpg" class="some-class" alt="" />',
105 96 # inline styles should be stripped
106 97 'with style !{width:100px;height100px}http://foo.bar/image.jpg!' => 'with style <img src="http://foo.bar/image.jpg" alt="" />',
107 98 'with title !http://foo.bar/image.jpg(This is a title)!' => 'with title <img src="http://foo.bar/image.jpg" title="This is a title" alt="This is a title" />',
108 99 'with title !http://foo.bar/image.jpg(This is a double-quoted "title")!' => 'with title <img src="http://foo.bar/image.jpg" title="This is a double-quoted &quot;title&quot;" alt="This is a double-quoted &quot;title&quot;" />',
109 100 }
110 101 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
111 102 end
112 103
113 104 def test_inline_images_inside_tags
114 105 raw = <<-RAW
115 106 h1. !foo.png! Heading
116 107
117 108 Centered image:
118 109
119 110 p=. !bar.gif!
120 111 RAW
121 112
122 113 assert textilizable(raw).include?('<img src="foo.png" alt="" />')
123 114 assert textilizable(raw).include?('<img src="bar.gif" alt="" />')
124 115 end
125 116
126 117 def test_acronyms
127 118 to_test = {
128 119 'this is an acronym: GPL(General Public License)' => 'this is an acronym: <acronym title="General Public License">GPL</acronym>',
129 120 'GPL(This is a double-quoted "title")' => '<acronym title="This is a double-quoted &quot;title&quot;">GPL</acronym>',
130 121 }
131 122 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
132 123
133 124 end
134 125
135 126 def test_attached_images
136 127 to_test = {
137 128 'Inline image: !logo.gif!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
138 129 'Inline image: !logo.GIF!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
139 130 'No match: !ogo.gif!' => 'No match: <img src="ogo.gif" alt="" />',
140 131 'No match: !ogo.GIF!' => 'No match: <img src="ogo.GIF" alt="" />',
141 132 # link image
142 133 '!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>',
143 134 }
144 135 attachments = Attachment.find(:all)
145 136 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
146 137 end
147 138
148 139 def test_textile_external_links
149 140 to_test = {
150 141 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>',
151 142 'This is an intern "link":/foo/bar' => 'This is an intern <a href="/foo/bar">link</a>',
152 143 '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>',
153 144 '"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>',
154 145 "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":</p>\n\n\n\t<p>Another paragraph",
155 146 # no multiline link text
156 147 "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",
157 148 # mailto link
158 149 "\"system administrator\":mailto:sysadmin@example.com?subject=redmine%20permissions" => "<a href=\"mailto:sysadmin@example.com?subject=redmine%20permissions\">system administrator</a>",
159 150 # two exclamation marks
160 151 '"a link":http://example.net/path!602815048C7B5C20!302.html' => '<a href="http://example.net/path!602815048C7B5C20!302.html" class="external">a link</a>',
161 152 # escaping
162 153 '"test":http://foo"bar' => '<a href="http://foo&quot;bar" class="external">test</a>',
163 154 }
164 155 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
165 156 end
166 157
167 158 def test_redmine_links
168 159 issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3},
169 160 :class => 'issue status-1 priority-1 overdue', :title => 'Error 281 when updating a recipe (New)')
170 161
171 162 changeset_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
172 163 :class => 'changeset', :title => 'My very first commit')
173 164 changeset_link2 = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
174 165 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
175 166
176 167 document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
177 168 :class => 'document')
178 169
179 170 version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
180 171 :class => 'version')
181 172
182 173 message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4}
183 174
184 175 project_url = {:controller => 'projects', :action => 'show', :id => 'subproject1'}
185 176
186 177 source_url = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}
187 178 source_url_with_ext = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file.ext']}
188 179
189 180 to_test = {
190 181 # tickets
191 182 '#3, [#3], (#3) and #3.' => "#{issue_link}, [#{issue_link}], (#{issue_link}) and #{issue_link}.",
192 183 # changesets
193 184 'r1' => changeset_link,
194 185 'r1.' => "#{changeset_link}.",
195 186 'r1, r2' => "#{changeset_link}, #{changeset_link2}",
196 187 'r1,r2' => "#{changeset_link},#{changeset_link2}",
197 188 # documents
198 189 'document#1' => document_link,
199 190 'document:"Test document"' => document_link,
200 191 # versions
201 192 'version#2' => version_link,
202 193 'version:1.0' => version_link,
203 194 'version:"1.0"' => version_link,
204 195 # source
205 196 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
206 197 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".",
207 198 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
208 199 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".",
209 200 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
210 201 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",",
211 202 'source:/some/file@52' => link_to('source:/some/file@52', source_url.merge(:rev => 52), :class => 'source'),
212 203 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_ext.merge(:rev => 52), :class => 'source'),
213 204 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url.merge(:anchor => 'L110'), :class => 'source'),
214 205 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext.merge(:anchor => 'L110'), :class => 'source'),
215 206 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url.merge(:rev => 52, :anchor => 'L110'), :class => 'source'),
216 207 'export:/some/file' => link_to('export:/some/file', source_url.merge(:format => 'raw'), :class => 'source download'),
217 208 # message
218 209 'message#4' => link_to('Post 2', message_url, :class => 'message'),
219 210 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5'), :class => 'message'),
220 211 # project
221 212 'project#3' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
222 213 'project:subproject1' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
223 214 'project:"eCookbook subProject 1"' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
224 215 # escaping
225 216 '!#3.' => '#3.',
226 217 '!r1' => 'r1',
227 218 '!document#1' => 'document#1',
228 219 '!document:"Test document"' => 'document:"Test document"',
229 220 '!version#2' => 'version#2',
230 221 '!version:1.0' => 'version:1.0',
231 222 '!version:"1.0"' => 'version:"1.0"',
232 223 '!source:/some/file' => 'source:/some/file',
233 224 # not found
234 225 '#0123456789' => '#0123456789',
235 226 # invalid expressions
236 227 'source:' => 'source:',
237 228 # url hash
238 229 "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
239 230 }
240 231 @project = Project.find(1)
241 232 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
242 233 end
243 234
244 235 def test_attachment_links
245 236 attachment_link = link_to('error281.txt', {:controller => 'attachments', :action => 'download', :id => '1'}, :class => 'attachment')
246 237 to_test = {
247 238 'attachment:error281.txt' => attachment_link
248 239 }
249 240 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => Issue.find(3).attachments), "#{text} failed" }
250 241 end
251 242
252 243 def test_wiki_links
253 244 to_test = {
254 245 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
255 246 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
256 247 # link with anchor
257 248 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
258 249 '[[Another page#anchor|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page#anchor" class="wiki-page">Page</a>',
259 250 # page that doesn't exist
260 251 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
261 252 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">404</a>',
262 253 # link to another project wiki
263 254 '[[onlinestore:]]' => '<a href="/projects/onlinestore/wiki/" class="wiki-page">onlinestore</a>',
264 255 '[[onlinestore:|Wiki]]' => '<a href="/projects/onlinestore/wiki/" class="wiki-page">Wiki</a>',
265 256 '[[onlinestore:Start page]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Start page</a>',
266 257 '[[onlinestore:Start page|Text]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Text</a>',
267 258 '[[onlinestore:Unknown page]]' => '<a href="/projects/onlinestore/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
268 259 # striked through link
269 260 '-[[Another page|Page]]-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a></del>',
270 261 '-[[Another page|Page]] link-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a> link</del>',
271 262 # escaping
272 263 '![[Another page|Page]]' => '[[Another page|Page]]',
273 264 # project does not exist
274 265 '[[unknowproject:Start]]' => '[[unknowproject:Start]]',
275 266 '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]',
276 267 }
277 268 @project = Project.find(1)
278 269 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
279 270 end
280 271
281 272 def test_html_tags
282 273 to_test = {
283 274 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
284 275 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
285 276 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
286 277 # do not escape pre/code tags
287 278 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
288 279 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
289 280 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
290 281 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
291 282 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
292 283 # remove attributes except class
293 284 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
294 285 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
295 286 }
296 287 to_test.each { |text, result| assert_equal result, textilizable(text) }
297 288 end
298 289
299 290 def test_allowed_html_tags
300 291 to_test = {
301 292 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
302 293 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
303 294 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
304 295 }
305 296 to_test.each { |text, result| assert_equal result, textilizable(text) }
306 297 end
307 298
308 299 def test_pre_tags
309 300 raw = <<-RAW
310 301 Before
311 302
312 303 <pre>
313 304 <prepared-statement-cache-size>32</prepared-statement-cache-size>
314 305 </pre>
315 306
316 307 After
317 308 RAW
318 309
319 310 expected = <<-EXPECTED
320 311 <p>Before</p>
321 312 <pre>
322 313 &lt;prepared-statement-cache-size&gt;32&lt;/prepared-statement-cache-size&gt;
323 314 </pre>
324 315 <p>After</p>
325 316 EXPECTED
326 317
327 318 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
328 319 end
329 320
330 321 def test_pre_content_should_not_parse_wiki_and_redmine_links
331 322 raw = <<-RAW
332 323 [[CookBook documentation]]
333 324
334 325 #1
335 326
336 327 <pre>
337 328 [[CookBook documentation]]
338 329
339 330 #1
340 331 </pre>
341 332 RAW
342 333
343 334 expected = <<-EXPECTED
344 335 <p><a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a></p>
345 336 <p><a href="/issues/1" class="issue status-1 priority-1" title="Can't print recipes (New)">#1</a></p>
346 337 <pre>
347 338 [[CookBook documentation]]
348 339
349 340 #1
350 341 </pre>
351 342 EXPECTED
352 343
353 344 @project = Project.find(1)
354 345 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
355 346 end
356 347
357 348 def test_non_closing_pre_blocks_should_be_closed
358 349 raw = <<-RAW
359 350 <pre><code>
360 351 RAW
361 352
362 353 expected = <<-EXPECTED
363 354 <pre><code>
364 355 </code></pre>
365 356 EXPECTED
366 357
367 358 @project = Project.find(1)
368 359 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
369 360 end
370 361
371 362 def test_syntax_highlight
372 363 raw = <<-RAW
373 364 <pre><code class="ruby">
374 365 # Some ruby code here
375 366 </code></pre>
376 367 RAW
377 368
378 369 expected = <<-EXPECTED
379 370 <pre><code class="ruby syntaxhl"><span class=\"CodeRay\"><span class="no">1</span> <span class="c"># Some ruby code here</span></span>
380 371 </code></pre>
381 372 EXPECTED
382 373
383 374 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
384 375 end
385 376
386 377 def test_wiki_links_in_tables
387 378 to_test = {"|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" =>
388 379 '<tr><td><a href="/projects/ecookbook/wiki/Page" class="wiki-page new">Link title</a></td>' +
389 380 '<td><a href="/projects/ecookbook/wiki/Other_Page" class="wiki-page new">Other title</a></td>' +
390 381 '</tr><tr><td>Cell 21</td><td><a href="/projects/ecookbook/wiki/Last_page" class="wiki-page new">Last page</a></td></tr>'
391 382 }
392 383 @project = Project.find(1)
393 384 to_test.each { |text, result| assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '') }
394 385 end
395 386
396 387 def test_text_formatting
397 388 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
398 389 '(_text within parentheses_)' => '(<em>text within parentheses</em>)',
399 390 'a *Humane Web* Text Generator' => 'a <strong>Humane Web</strong> Text Generator',
400 391 '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>',
401 392 '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',
402 393 }
403 394 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
404 395 end
405 396
406 397 def test_wiki_horizontal_rule
407 398 assert_equal '<hr />', textilizable('---')
408 399 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
409 400 end
410 401
411 402 def test_acronym
412 403 assert_equal '<p>This is an acronym: <acronym title="American Civil Liberties Union">ACLU</acronym>.</p>',
413 404 textilizable('This is an acronym: ACLU(American Civil Liberties Union).')
414 405 end
415 406
416 407 def test_footnotes
417 408 raw = <<-RAW
418 409 This is some text[1].
419 410
420 411 fn1. This is the foot note
421 412 RAW
422 413
423 414 expected = <<-EXPECTED
424 415 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
425 416 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
426 417 EXPECTED
427 418
428 419 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
429 420 end
430 421
431 422 def test_table_of_content
432 423 raw = <<-RAW
433 424 {{toc}}
434 425
435 426 h1. Title
436 427
437 428 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
438 429
439 430 h2. Subtitle with a [[Wiki]] link
440 431
441 432 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
442 433
443 434 h2. Subtitle with [[Wiki|another Wiki]] link
444 435
445 436 h2. Subtitle with %{color:red}red text%
446 437
447 438 h1. Another title
448 439
449 440 h2. An "Internet link":http://www.redmine.org/ inside subtitle
450 441
451 442 h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
452 443
453 444 RAW
454 445
455 446 expected = '<ul class="toc">' +
456 447 '<li class="heading1"><a href="#Title">Title</a></li>' +
457 448 '<li class="heading2"><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
458 449 '<li class="heading2"><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
459 450 '<li class="heading2"><a href="#Subtitle-with-red-text">Subtitle with red text</a></li>' +
460 451 '<li class="heading1"><a href="#Another-title">Another title</a></li>' +
461 452 '<li class="heading2"><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
462 453 '<li class="heading2"><a href="#Project-Name">Project Name</a></li>' +
463 454 '</ul>'
464 455
465 456 assert textilizable(raw).gsub("\n", "").include?(expected)
466 457 end
467 458
468 459 def test_blockquote
469 460 # orig raw text
470 461 raw = <<-RAW
471 462 John said:
472 463 > Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
473 464 > Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
474 465 > * Donec odio lorem,
475 466 > * sagittis ac,
476 467 > * malesuada in,
477 468 > * adipiscing eu, dolor.
478 469 >
479 470 > >Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.
480 471 > Proin a tellus. Nam vel neque.
481 472
482 473 He's right.
483 474 RAW
484 475
485 476 # expected html
486 477 expected = <<-EXPECTED
487 478 <p>John said:</p>
488 479 <blockquote>
489 480 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
490 481 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
491 482 <ul>
492 483 <li>Donec odio lorem,</li>
493 484 <li>sagittis ac,</li>
494 485 <li>malesuada in,</li>
495 486 <li>adipiscing eu, dolor.</li>
496 487 </ul>
497 488 <blockquote>
498 489 <p>Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.</p>
499 490 </blockquote>
500 491 <p>Proin a tellus. Nam vel neque.</p>
501 492 </blockquote>
502 493 <p>He's right.</p>
503 494 EXPECTED
504 495
505 496 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
506 497 end
507 498
508 499 def test_table
509 500 raw = <<-RAW
510 501 This is a table with empty cells:
511 502
512 503 |cell11|cell12||
513 504 |cell21||cell23|
514 505 |cell31|cell32|cell33|
515 506 RAW
516 507
517 508 expected = <<-EXPECTED
518 509 <p>This is a table with empty cells:</p>
519 510
520 511 <table>
521 512 <tr><td>cell11</td><td>cell12</td><td></td></tr>
522 513 <tr><td>cell21</td><td></td><td>cell23</td></tr>
523 514 <tr><td>cell31</td><td>cell32</td><td>cell33</td></tr>
524 515 </table>
525 516 EXPECTED
526 517
527 518 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
528 519 end
529 520
530 521 def test_table_with_line_breaks
531 522 raw = <<-RAW
532 523 This is a table with line breaks:
533 524
534 525 |cell11
535 526 continued|cell12||
536 527 |-cell21-||cell23
537 528 cell23 line2
538 529 cell23 *line3*|
539 530 |cell31|cell32
540 531 cell32 line2|cell33|
541 532
542 533 RAW
543 534
544 535 expected = <<-EXPECTED
545 536 <p>This is a table with line breaks:</p>
546 537
547 538 <table>
548 539 <tr>
549 540 <td>cell11<br />continued</td>
550 541 <td>cell12</td>
551 542 <td></td>
552 543 </tr>
553 544 <tr>
554 545 <td><del>cell21</del></td>
555 546 <td></td>
556 547 <td>cell23<br/>cell23 line2<br/>cell23 <strong>line3</strong></td>
557 548 </tr>
558 549 <tr>
559 550 <td>cell31</td>
560 551 <td>cell32<br/>cell32 line2</td>
561 552 <td>cell33</td>
562 553 </tr>
563 554 </table>
564 555 EXPECTED
565 556
566 557 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
567 558 end
568 559
569 560 def test_textile_should_not_mangle_brackets
570 561 assert_equal '<p>[msg1][msg2]</p>', textilizable('[msg1][msg2]')
571 562 end
572 563
573 564 def test_default_formatter
574 565 Setting.text_formatting = 'unknown'
575 566 text = 'a *link*: http://www.example.net/'
576 567 assert_equal '<p>a *link*: <a href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
577 568 Setting.text_formatting = 'textile'
578 569 end
579 570
580 571 def test_due_date_distance_in_words
581 572 to_test = { Date.today => 'Due in 0 days',
582 573 Date.today + 1 => 'Due in 1 day',
583 574 Date.today + 100 => 'Due in about 3 months',
584 575 Date.today + 20000 => 'Due in over 54 years',
585 576 Date.today - 1 => '1 day late',
586 577 Date.today - 100 => 'about 3 months late',
587 578 Date.today - 20000 => 'over 54 years late',
588 579 }
589 580 to_test.each do |date, expected|
590 581 assert_equal expected, due_date_distance_in_words(date)
591 582 end
592 583 end
593 584
594 585 def test_avatar
595 586 # turn on avatars
596 587 Setting.gravatar_enabled = '1'
597 588 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
598 589 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
599 590 assert_nil avatar('jsmith')
600 591 assert_nil avatar(nil)
601 592
602 593 # turn off avatars
603 594 Setting.gravatar_enabled = '0'
604 595 assert_nil avatar(User.find_by_mail('jsmith@somenet.foo'))
605 596 end
606 597
607 598 def test_link_to_user
608 599 user = User.find(2)
609 600 t = link_to_user(user)
610 601 assert_equal "<a href=\"/users/2\">#{ user.name }</a>", t
611 602 end
612 603
613 604 def test_link_to_user_should_not_link_to_locked_user
614 605 user = User.find(5)
615 606 assert user.locked?
616 607 t = link_to_user(user)
617 608 assert_equal user.name, t
618 609 end
619 610
620 611 def test_link_to_user_should_not_link_to_anonymous
621 612 user = User.anonymous
622 613 assert user.anonymous?
623 614 t = link_to_user(user)
624 615 assert_equal ::I18n.t(:label_user_anonymous), t
625 616 end
626 617
627 618 def test_link_to_project
628 619 project = Project.find(1)
629 620 assert_equal %(<a href="/projects/ecookbook">eCookbook</a>),
630 621 link_to_project(project)
631 622 assert_equal %(<a href="/projects/ecookbook/settings">eCookbook</a>),
632 623 link_to_project(project, :action => 'settings')
633 624 assert_equal %(<a href="http://test.host/projects/ecookbook?jump=blah">eCookbook</a>),
634 625 link_to_project(project, {:only_path => false, :jump => 'blah'})
635 626 assert_equal %(<a href="/projects/ecookbook/settings" class="project">eCookbook</a>),
636 627 link_to_project(project, {:action => 'settings'}, :class => "project")
637 628 end
638 629 end
General Comments 0
You need to be logged in to leave comments. Login now