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