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