##// END OF EJS Templates
Revert part of r4064....
Eric Davis -
r4143:5823a71c1851
parent child
Show More
@@ -1,864 +1,850
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 def link_to_project(project, options={})
126 112 options[:class] ||= 'project'
127 113 link_to(h(project), {:controller => 'projects', :action => 'show', :id => project}, :class => options[:class])
128 114 end
129 115
130 116 # Generates a link to a project if active
131 117 # Examples:
132 118 #
133 119 # link_to_project(project) # => link to the specified project overview
134 120 # link_to_project(project, :action=>'settings') # => link to project settings
135 121 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
136 122 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
137 123 #
138 124 def link_to_project(project, options={}, html_options = nil)
139 125 if project.active?
140 126 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
141 127 link_to(h(project), url, html_options)
142 128 else
143 129 h(project)
144 130 end
145 131 end
146 132
147 133 def toggle_link(name, id, options={})
148 134 onclick = "Element.toggle('#{id}'); "
149 135 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
150 136 onclick << "return false;"
151 137 link_to(name, "#", :onclick => onclick)
152 138 end
153 139
154 140 def image_to_function(name, function, html_options = {})
155 141 html_options.symbolize_keys!
156 142 tag(:input, html_options.merge({
157 143 :type => "image", :src => image_path(name),
158 144 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
159 145 }))
160 146 end
161 147
162 148 def prompt_to_remote(name, text, param, url, html_options = {})
163 149 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
164 150 link_to name, {}, html_options
165 151 end
166 152
167 153 def format_activity_title(text)
168 154 h(truncate_single_line(text, :length => 100))
169 155 end
170 156
171 157 def format_activity_day(date)
172 158 date == Date.today ? l(:label_today).titleize : format_date(date)
173 159 end
174 160
175 161 def format_activity_description(text)
176 162 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
177 163 end
178 164
179 165 def format_version_name(version)
180 166 if version.project == @project
181 167 h(version)
182 168 else
183 169 h("#{version.project} - #{version}")
184 170 end
185 171 end
186 172
187 173 def due_date_distance_in_words(date)
188 174 if date
189 175 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
190 176 end
191 177 end
192 178
193 179 def render_page_hierarchy(pages, node=nil)
194 180 content = ''
195 181 if pages[node]
196 182 content << "<ul class=\"pages-hierarchy\">\n"
197 183 pages[node].each do |page|
198 184 content << "<li>"
199 185 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'index', :id => page.project, :page => page.title},
200 186 :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
201 187 content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
202 188 content << "</li>\n"
203 189 end
204 190 content << "</ul>\n"
205 191 end
206 192 content
207 193 end
208 194
209 195 # Renders flash messages
210 196 def render_flash_messages
211 197 s = ''
212 198 flash.each do |k,v|
213 199 s << content_tag('div', v, :class => "flash #{k}")
214 200 end
215 201 s
216 202 end
217 203
218 204 # Renders tabs and their content
219 205 def render_tabs(tabs)
220 206 if tabs.any?
221 207 render :partial => 'common/tabs', :locals => {:tabs => tabs}
222 208 else
223 209 content_tag 'p', l(:label_no_data), :class => "nodata"
224 210 end
225 211 end
226 212
227 213 # Renders the project quick-jump box
228 214 def render_project_jump_box
229 215 # Retrieve them now to avoid a COUNT query
230 216 projects = User.current.projects.all
231 217 if projects.any?
232 218 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
233 219 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
234 220 '<option value="" disabled="disabled">---</option>'
235 221 s << project_tree_options_for_select(projects, :selected => @project) do |p|
236 222 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
237 223 end
238 224 s << '</select>'
239 225 s
240 226 end
241 227 end
242 228
243 229 def project_tree_options_for_select(projects, options = {})
244 230 s = ''
245 231 project_tree(projects) do |project, level|
246 232 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
247 233 tag_options = {:value => project.id}
248 234 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
249 235 tag_options[:selected] = 'selected'
250 236 else
251 237 tag_options[:selected] = nil
252 238 end
253 239 tag_options.merge!(yield(project)) if block_given?
254 240 s << content_tag('option', name_prefix + h(project), tag_options)
255 241 end
256 242 s
257 243 end
258 244
259 245 # Yields the given block for each project with its level in the tree
260 246 def project_tree(projects, &block)
261 247 ancestors = []
262 248 projects.sort_by(&:lft).each do |project|
263 249 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
264 250 ancestors.pop
265 251 end
266 252 yield project, ancestors.size
267 253 ancestors << project
268 254 end
269 255 end
270 256
271 257 def project_nested_ul(projects, &block)
272 258 s = ''
273 259 if projects.any?
274 260 ancestors = []
275 261 projects.sort_by(&:lft).each do |project|
276 262 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
277 263 s << "<ul>\n"
278 264 else
279 265 ancestors.pop
280 266 s << "</li>"
281 267 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
282 268 ancestors.pop
283 269 s << "</ul></li>\n"
284 270 end
285 271 end
286 272 s << "<li>"
287 273 s << yield(project).to_s
288 274 ancestors << project
289 275 end
290 276 s << ("</li></ul>\n" * ancestors.size)
291 277 end
292 278 s
293 279 end
294 280
295 281 def principals_check_box_tags(name, principals)
296 282 s = ''
297 283 principals.sort.each do |principal|
298 284 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
299 285 end
300 286 s
301 287 end
302 288
303 289 # Truncates and returns the string as a single line
304 290 def truncate_single_line(string, *args)
305 291 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
306 292 end
307 293
308 294 # Truncates at line break after 250 characters or options[:length]
309 295 def truncate_lines(string, options={})
310 296 length = options[:length] || 250
311 297 if string.to_s =~ /\A(.{#{length}}.*?)$/m
312 298 "#{$1}..."
313 299 else
314 300 string
315 301 end
316 302 end
317 303
318 304 def html_hours(text)
319 305 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
320 306 end
321 307
322 308 def authoring(created, author, options={})
323 309 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created))
324 310 end
325 311
326 312 def time_tag(time)
327 313 text = distance_of_time_in_words(Time.now, time)
328 314 if @project
329 315 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
330 316 else
331 317 content_tag('acronym', text, :title => format_time(time))
332 318 end
333 319 end
334 320
335 321 def syntax_highlight(name, content)
336 322 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
337 323 end
338 324
339 325 def to_path_param(path)
340 326 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
341 327 end
342 328
343 329 def pagination_links_full(paginator, count=nil, options={})
344 330 page_param = options.delete(:page_param) || :page
345 331 per_page_links = options.delete(:per_page_links)
346 332 url_param = params.dup
347 333 # don't reuse query params if filters are present
348 334 url_param.merge!(:fields => nil, :values => nil, :operators => nil) if url_param.delete(:set_filter)
349 335
350 336 html = ''
351 337 if paginator.current.previous
352 338 html << link_to_remote_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
353 339 end
354 340
355 341 html << (pagination_links_each(paginator, options) do |n|
356 342 link_to_remote_content_update(n.to_s, url_param.merge(page_param => n))
357 343 end || '')
358 344
359 345 if paginator.current.next
360 346 html << ' ' + link_to_remote_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next))
361 347 end
362 348
363 349 unless count.nil?
364 350 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
365 351 if per_page_links != false && links = per_page_links(paginator.items_per_page)
366 352 html << " | #{links}"
367 353 end
368 354 end
369 355
370 356 html
371 357 end
372 358
373 359 def per_page_links(selected=nil)
374 360 url_param = params.dup
375 361 url_param.clear if url_param.has_key?(:set_filter)
376 362
377 363 links = Setting.per_page_options_array.collect do |n|
378 364 n == selected ? n : link_to_remote(n, {:update => "content",
379 365 :url => params.dup.merge(:per_page => n),
380 366 :method => :get},
381 367 {:href => url_for(url_param.merge(:per_page => n))})
382 368 end
383 369 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
384 370 end
385 371
386 372 def reorder_links(name, url)
387 373 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), url.merge({"#{name}[move_to]" => 'highest'}), :method => :post, :title => l(:label_sort_highest)) +
388 374 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), url.merge({"#{name}[move_to]" => 'higher'}), :method => :post, :title => l(:label_sort_higher)) +
389 375 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), url.merge({"#{name}[move_to]" => 'lower'}), :method => :post, :title => l(:label_sort_lower)) +
390 376 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), url.merge({"#{name}[move_to]" => 'lowest'}), :method => :post, :title => l(:label_sort_lowest))
391 377 end
392 378
393 379 def breadcrumb(*args)
394 380 elements = args.flatten
395 381 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
396 382 end
397 383
398 384 def other_formats_links(&block)
399 385 concat('<p class="other-formats">' + l(:label_export_to))
400 386 yield Redmine::Views::OtherFormatsBuilder.new(self)
401 387 concat('</p>')
402 388 end
403 389
404 390 def page_header_title
405 391 if @project.nil? || @project.new_record?
406 392 h(Setting.app_title)
407 393 else
408 394 b = []
409 395 ancestors = (@project.root? ? [] : @project.ancestors.visible)
410 396 if ancestors.any?
411 397 root = ancestors.shift
412 398 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
413 399 if ancestors.size > 2
414 400 b << '&#8230;'
415 401 ancestors = ancestors[-2, 2]
416 402 end
417 403 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
418 404 end
419 405 b << h(@project)
420 406 b.join(' &#187; ')
421 407 end
422 408 end
423 409
424 410 def html_title(*args)
425 411 if args.empty?
426 412 title = []
427 413 title << @project.name if @project
428 414 title += @html_title if @html_title
429 415 title << Setting.app_title
430 416 title.select {|t| !t.blank? }.join(' - ')
431 417 else
432 418 @html_title ||= []
433 419 @html_title += args
434 420 end
435 421 end
436 422
437 423 # Returns the theme, controller name, and action as css classes for the
438 424 # HTML body.
439 425 def body_css_classes
440 426 css = []
441 427 if theme = Redmine::Themes.theme(Setting.ui_theme)
442 428 css << 'theme-' + theme.name
443 429 end
444 430
445 431 css << 'controller-' + params[:controller]
446 432 css << 'action-' + params[:action]
447 433 css.join(' ')
448 434 end
449 435
450 436 def accesskey(s)
451 437 Redmine::AccessKeys.key_for s
452 438 end
453 439
454 440 # Formats text according to system settings.
455 441 # 2 ways to call this method:
456 442 # * with a String: textilizable(text, options)
457 443 # * with an object and one of its attribute: textilizable(issue, :description, options)
458 444 def textilizable(*args)
459 445 options = args.last.is_a?(Hash) ? args.pop : {}
460 446 case args.size
461 447 when 1
462 448 obj = options[:object]
463 449 text = args.shift
464 450 when 2
465 451 obj = args.shift
466 452 attr = args.shift
467 453 text = obj.send(attr).to_s
468 454 else
469 455 raise ArgumentError, 'invalid arguments to textilizable'
470 456 end
471 457 return '' if text.blank?
472 458 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
473 459 only_path = options.delete(:only_path) == false ? false : true
474 460
475 461 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }
476 462
477 463 parse_non_pre_blocks(text) do |text|
478 464 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
479 465 send method_name, text, project, obj, attr, only_path, options
480 466 end
481 467 end
482 468 end
483 469
484 470 def parse_non_pre_blocks(text)
485 471 s = StringScanner.new(text)
486 472 tags = []
487 473 parsed = ''
488 474 while !s.eos?
489 475 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
490 476 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
491 477 if tags.empty?
492 478 yield text
493 479 end
494 480 parsed << text
495 481 if tag
496 482 if closing
497 483 if tags.last == tag.downcase
498 484 tags.pop
499 485 end
500 486 else
501 487 tags << tag.downcase
502 488 end
503 489 parsed << full_tag
504 490 end
505 491 end
506 492 # Close any non closing tags
507 493 while tag = tags.pop
508 494 parsed << "</#{tag}>"
509 495 end
510 496 parsed
511 497 end
512 498
513 499 def parse_inline_attachments(text, project, obj, attr, only_path, options)
514 500 # when using an image link, try to use an attachment, if possible
515 501 if options[:attachments] || (obj && obj.respond_to?(:attachments))
516 502 attachments = nil
517 503 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
518 504 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
519 505 attachments ||= (options[:attachments] || obj.attachments).sort_by(&:created_on).reverse
520 506 # search for the picture in attachments
521 507 if found = attachments.detect { |att| att.filename.downcase == filename }
522 508 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
523 509 desc = found.description.to_s.gsub('"', '')
524 510 if !desc.blank? && alttext.blank?
525 511 alt = " title=\"#{desc}\" alt=\"#{desc}\""
526 512 end
527 513 "src=\"#{image_url}\"#{alt}"
528 514 else
529 515 m
530 516 end
531 517 end
532 518 end
533 519 end
534 520
535 521 # Wiki links
536 522 #
537 523 # Examples:
538 524 # [[mypage]]
539 525 # [[mypage|mytext]]
540 526 # wiki links can refer other project wikis, using project name or identifier:
541 527 # [[project:]] -> wiki starting page
542 528 # [[project:|mytext]]
543 529 # [[project:mypage]]
544 530 # [[project:mypage|mytext]]
545 531 def parse_wiki_links(text, project, obj, attr, only_path, options)
546 532 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
547 533 link_project = project
548 534 esc, all, page, title = $1, $2, $3, $5
549 535 if esc.nil?
550 536 if page =~ /^([^\:]+)\:(.*)$/
551 537 link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
552 538 page = $2
553 539 title ||= $1 if page.blank?
554 540 end
555 541
556 542 if link_project && link_project.wiki
557 543 # extract anchor
558 544 anchor = nil
559 545 if page =~ /^(.+?)\#(.+)$/
560 546 page, anchor = $1, $2
561 547 end
562 548 # check if page exists
563 549 wiki_page = link_project.wiki.find_page(page)
564 550 url = case options[:wiki_links]
565 551 when :local; "#{title}.html"
566 552 when :anchor; "##{title}" # used for single-file wiki export
567 553 else
568 554 url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => link_project, :page => Wiki.titleize(page), :anchor => anchor)
569 555 end
570 556 link_to((title || page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
571 557 else
572 558 # project or wiki doesn't exist
573 559 all
574 560 end
575 561 else
576 562 all
577 563 end
578 564 end
579 565 end
580 566
581 567 # Redmine links
582 568 #
583 569 # Examples:
584 570 # Issues:
585 571 # #52 -> Link to issue #52
586 572 # Changesets:
587 573 # r52 -> Link to revision 52
588 574 # commit:a85130f -> Link to scmid starting with a85130f
589 575 # Documents:
590 576 # document#17 -> Link to document with id 17
591 577 # document:Greetings -> Link to the document with title "Greetings"
592 578 # document:"Some document" -> Link to the document with title "Some document"
593 579 # Versions:
594 580 # version#3 -> Link to version with id 3
595 581 # version:1.0.0 -> Link to version named "1.0.0"
596 582 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
597 583 # Attachments:
598 584 # attachment:file.zip -> Link to the attachment of the current object named file.zip
599 585 # Source files:
600 586 # source:some/file -> Link to the file located at /some/file in the project's repository
601 587 # source:some/file@52 -> Link to the file's revision 52
602 588 # source:some/file#L120 -> Link to line 120 of the file
603 589 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
604 590 # export:some/file -> Force the download of the file
605 591 # Forum messages:
606 592 # message#1218 -> Link to message with id 1218
607 593 def parse_redmine_links(text, project, obj, attr, only_path, options)
608 594 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(attachment|document|version|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m|
609 595 leading, esc, prefix, sep, identifier = $1, $2, $3, $5 || $7, $6 || $8
610 596 link = nil
611 597 if esc.nil?
612 598 if prefix.nil? && sep == 'r'
613 599 if project && (changeset = project.changesets.find_by_revision(identifier))
614 600 link = link_to("r#{identifier}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
615 601 :class => 'changeset',
616 602 :title => truncate_single_line(changeset.comments, :length => 100))
617 603 end
618 604 elsif sep == '#'
619 605 oid = identifier.to_i
620 606 case prefix
621 607 when nil
622 608 if issue = Issue.visible.find_by_id(oid, :include => :status)
623 609 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
624 610 :class => issue.css_classes,
625 611 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
626 612 end
627 613 when 'document'
628 614 if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
629 615 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
630 616 :class => 'document'
631 617 end
632 618 when 'version'
633 619 if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
634 620 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
635 621 :class => 'version'
636 622 end
637 623 when 'message'
638 624 if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
639 625 link = link_to h(truncate(message.subject, :length => 60)), {:only_path => only_path,
640 626 :controller => 'messages',
641 627 :action => 'show',
642 628 :board_id => message.board,
643 629 :id => message.root,
644 630 :anchor => (message.parent ? "message-#{message.id}" : nil)},
645 631 :class => 'message'
646 632 end
647 633 when 'project'
648 634 if p = Project.visible.find_by_id(oid)
649 635 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
650 636 end
651 637 end
652 638 elsif sep == ':'
653 639 # removes the double quotes if any
654 640 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
655 641 case prefix
656 642 when 'document'
657 643 if project && document = project.documents.find_by_title(name)
658 644 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
659 645 :class => 'document'
660 646 end
661 647 when 'version'
662 648 if project && version = project.versions.find_by_name(name)
663 649 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
664 650 :class => 'version'
665 651 end
666 652 when 'commit'
667 653 if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
668 654 link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
669 655 :class => 'changeset',
670 656 :title => truncate_single_line(changeset.comments, :length => 100)
671 657 end
672 658 when 'source', 'export'
673 659 if project && project.repository
674 660 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
675 661 path, rev, anchor = $1, $3, $5
676 662 link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
677 663 :path => to_path_param(path),
678 664 :rev => rev,
679 665 :anchor => anchor,
680 666 :format => (prefix == 'export' ? 'raw' : nil)},
681 667 :class => (prefix == 'export' ? 'source download' : 'source')
682 668 end
683 669 when 'attachment'
684 670 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
685 671 if attachments && attachment = attachments.detect {|a| a.filename == name }
686 672 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
687 673 :class => 'attachment'
688 674 end
689 675 when 'project'
690 676 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
691 677 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
692 678 end
693 679 end
694 680 end
695 681 end
696 682 leading + (link || "#{prefix}#{sep}#{identifier}")
697 683 end
698 684 end
699 685
700 686 # Same as Rails' simple_format helper without using paragraphs
701 687 def simple_format_without_paragraph(text)
702 688 text.to_s.
703 689 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
704 690 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
705 691 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
706 692 end
707 693
708 694 def lang_options_for_select(blank=true)
709 695 (blank ? [["(auto)", ""]] : []) +
710 696 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
711 697 end
712 698
713 699 def label_tag_for(name, option_tags = nil, options = {})
714 700 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
715 701 content_tag("label", label_text)
716 702 end
717 703
718 704 def labelled_tabular_form_for(name, object, options, &proc)
719 705 options[:html] ||= {}
720 706 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
721 707 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
722 708 end
723 709
724 710 def back_url_hidden_field_tag
725 711 back_url = params[:back_url] || request.env['HTTP_REFERER']
726 712 back_url = CGI.unescape(back_url.to_s)
727 713 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
728 714 end
729 715
730 716 def check_all_links(form_name)
731 717 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
732 718 " | " +
733 719 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
734 720 end
735 721
736 722 def progress_bar(pcts, options={})
737 723 pcts = [pcts, pcts] unless pcts.is_a?(Array)
738 724 pcts = pcts.collect(&:round)
739 725 pcts[1] = pcts[1] - pcts[0]
740 726 pcts << (100 - pcts[1] - pcts[0])
741 727 width = options[:width] || '100px;'
742 728 legend = options[:legend] || ''
743 729 content_tag('table',
744 730 content_tag('tr',
745 731 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : '') +
746 732 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : '') +
747 733 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : '')
748 734 ), :class => 'progress', :style => "width: #{width};") +
749 735 content_tag('p', legend, :class => 'pourcent')
750 736 end
751 737
752 738 def checked_image(checked=true)
753 739 if checked
754 740 image_tag 'toggle_check.png'
755 741 end
756 742 end
757 743
758 744 def context_menu(url)
759 745 unless @context_menu_included
760 746 content_for :header_tags do
761 747 javascript_include_tag('context_menu') +
762 748 stylesheet_link_tag('context_menu')
763 749 end
764 750 if l(:direction) == 'rtl'
765 751 content_for :header_tags do
766 752 stylesheet_link_tag('context_menu_rtl')
767 753 end
768 754 end
769 755 @context_menu_included = true
770 756 end
771 757 javascript_tag "new ContextMenu('#{ url_for(url) }')"
772 758 end
773 759
774 760 def context_menu_link(name, url, options={})
775 761 options[:class] ||= ''
776 762 if options.delete(:selected)
777 763 options[:class] << ' icon-checked disabled'
778 764 options[:disabled] = true
779 765 end
780 766 if options.delete(:disabled)
781 767 options.delete(:method)
782 768 options.delete(:confirm)
783 769 options.delete(:onclick)
784 770 options[:class] << ' disabled'
785 771 url = '#'
786 772 end
787 773 link_to name, url, options
788 774 end
789 775
790 776 def calendar_for(field_id)
791 777 include_calendar_headers_tags
792 778 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
793 779 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
794 780 end
795 781
796 782 def include_calendar_headers_tags
797 783 unless @calendar_headers_tags_included
798 784 @calendar_headers_tags_included = true
799 785 content_for :header_tags do
800 786 start_of_week = case Setting.start_of_week.to_i
801 787 when 1
802 788 'Calendar._FD = 1;' # Monday
803 789 when 7
804 790 'Calendar._FD = 0;' # Sunday
805 791 else
806 792 '' # use language
807 793 end
808 794
809 795 javascript_include_tag('calendar/calendar') +
810 796 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
811 797 javascript_tag(start_of_week) +
812 798 javascript_include_tag('calendar/calendar-setup') +
813 799 stylesheet_link_tag('calendar')
814 800 end
815 801 end
816 802 end
817 803
818 804 def content_for(name, content = nil, &block)
819 805 @has_content ||= {}
820 806 @has_content[name] = true
821 807 super(name, content, &block)
822 808 end
823 809
824 810 def has_content?(name)
825 811 (@has_content && @has_content[name]) || false
826 812 end
827 813
828 814 # Returns the avatar image tag for the given +user+ if avatars are enabled
829 815 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
830 816 def avatar(user, options = { })
831 817 if Setting.gravatar_enabled?
832 818 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
833 819 email = nil
834 820 if user.respond_to?(:mail)
835 821 email = user.mail
836 822 elsif user.to_s =~ %r{<(.+?)>}
837 823 email = $1
838 824 end
839 825 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
840 826 else
841 827 ''
842 828 end
843 829 end
844 830
845 831 def favicon
846 832 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />"
847 833 end
848 834
849 835 private
850 836
851 837 def wiki_helper
852 838 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
853 839 extend helper
854 840 return self
855 841 end
856 842
857 843 def link_to_remote_content_update(text, url_params)
858 844 link_to_remote(text,
859 845 {:url => url_params, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'},
860 846 {:href => url_for(:params => url_params)}
861 847 )
862 848 end
863 849
864 850 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_equal '', 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