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