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