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