##// END OF EJS Templates
Adds #favicon_path and #favicon_url helpers....
Jean-Philippe Lang -
r12386:1eda38be4ce0
parent child
Show More
@@ -1,1314 +1,1327
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 only_path = options[:only_path].nil? ? true : options[:only_path]
83 83 s = link_to text, issue_path(issue, :only_path => only_path), :class => issue.css_classes, :title => title
84 84 s << h(": #{subject}") if subject
85 85 s = h("#{issue.project} - ") + s if options[:project]
86 86 s
87 87 end
88 88
89 89 # Generates a link to an attachment.
90 90 # Options:
91 91 # * :text - Link text (default to attachment filename)
92 92 # * :download - Force download (default: false)
93 93 def link_to_attachment(attachment, options={})
94 94 text = options.delete(:text) || attachment.filename
95 95 route_method = options.delete(:download) ? :download_named_attachment_path : :named_attachment_path
96 96 html_options = options.slice!(:only_path)
97 97 url = send(route_method, attachment, attachment.filename, options)
98 98 link_to text, url, html_options
99 99 end
100 100
101 101 # Generates a link to a SCM revision
102 102 # Options:
103 103 # * :text - Link text (default to the formatted revision)
104 104 def link_to_revision(revision, repository, options={})
105 105 if repository.is_a?(Project)
106 106 repository = repository.repository
107 107 end
108 108 text = options.delete(:text) || format_revision(revision)
109 109 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
110 110 link_to(
111 111 h(text),
112 112 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
113 113 :title => l(:label_revision_id, format_revision(revision))
114 114 )
115 115 end
116 116
117 117 # Generates a link to a message
118 118 def link_to_message(message, options={}, html_options = nil)
119 119 link_to(
120 120 truncate(message.subject, :length => 60),
121 121 board_message_path(message.board_id, message.parent_id || message.id, {
122 122 :r => (message.parent_id && message.id),
123 123 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
124 124 }.merge(options)),
125 125 html_options
126 126 )
127 127 end
128 128
129 129 # Generates a link to a project if active
130 130 # Examples:
131 131 #
132 132 # link_to_project(project) # => link to the specified project overview
133 133 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
134 134 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
135 135 #
136 136 def link_to_project(project, options={}, html_options = nil)
137 137 if project.archived?
138 138 h(project.name)
139 139 elsif options.key?(:action)
140 140 ActiveSupport::Deprecation.warn "#link_to_project with :action option is deprecated and will be removed in Redmine 3.0."
141 141 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
142 142 link_to project.name, url, html_options
143 143 else
144 144 link_to project.name, project_path(project, options), html_options
145 145 end
146 146 end
147 147
148 148 # Generates a link to a project settings if active
149 149 def link_to_project_settings(project, options={}, html_options=nil)
150 150 if project.active?
151 151 link_to project.name, settings_project_path(project, options), html_options
152 152 elsif project.archived?
153 153 h(project.name)
154 154 else
155 155 link_to project.name, project_path(project, options), html_options
156 156 end
157 157 end
158 158
159 159 # Helper that formats object for html or text rendering
160 160 def format_object(object, html=true)
161 161 case object.class.name
162 162 when 'Array'
163 163 object.map {|o| format_object(o, html)}.join(', ').html_safe
164 164 when 'Time'
165 165 format_time(object)
166 166 when 'Date'
167 167 format_date(object)
168 168 when 'Fixnum'
169 169 object.to_s
170 170 when 'Float'
171 171 sprintf "%.2f", object
172 172 when 'User'
173 173 html ? link_to_user(object) : object.to_s
174 174 when 'Project'
175 175 html ? link_to_project(object) : object.to_s
176 176 when 'Version'
177 177 html ? link_to(object.name, version_path(object)) : object.to_s
178 178 when 'TrueClass'
179 179 l(:general_text_Yes)
180 180 when 'FalseClass'
181 181 l(:general_text_No)
182 182 when 'Issue'
183 183 object.visible? && html ? link_to_issue(object) : "##{object.id}"
184 184 when 'CustomValue', 'CustomFieldValue'
185 185 if object.custom_field
186 186 f = object.custom_field.format.formatted_custom_value(self, object, html)
187 187 if f.nil? || f.is_a?(String)
188 188 f
189 189 else
190 190 format_object(f, html)
191 191 end
192 192 else
193 193 object.value.to_s
194 194 end
195 195 else
196 196 html ? h(object) : object.to_s
197 197 end
198 198 end
199 199
200 200 def wiki_page_path(page, options={})
201 201 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
202 202 end
203 203
204 204 def thumbnail_tag(attachment)
205 205 link_to image_tag(thumbnail_path(attachment)),
206 206 named_attachment_path(attachment, attachment.filename),
207 207 :title => attachment.filename
208 208 end
209 209
210 210 def toggle_link(name, id, options={})
211 211 onclick = "$('##{id}').toggle(); "
212 212 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
213 213 onclick << "return false;"
214 214 link_to(name, "#", :onclick => onclick)
215 215 end
216 216
217 217 def image_to_function(name, function, html_options = {})
218 218 html_options.symbolize_keys!
219 219 tag(:input, html_options.merge({
220 220 :type => "image", :src => image_path(name),
221 221 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
222 222 }))
223 223 end
224 224
225 225 def format_activity_title(text)
226 226 h(truncate_single_line(text, :length => 100))
227 227 end
228 228
229 229 def format_activity_day(date)
230 230 date == User.current.today ? l(:label_today).titleize : format_date(date)
231 231 end
232 232
233 233 def format_activity_description(text)
234 234 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
235 235 ).gsub(/[\r\n]+/, "<br />").html_safe
236 236 end
237 237
238 238 def format_version_name(version)
239 239 if version.project == @project
240 240 h(version)
241 241 else
242 242 h("#{version.project} - #{version}")
243 243 end
244 244 end
245 245
246 246 def due_date_distance_in_words(date)
247 247 if date
248 248 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
249 249 end
250 250 end
251 251
252 252 # Renders a tree of projects as a nested set of unordered lists
253 253 # The given collection may be a subset of the whole project tree
254 254 # (eg. some intermediate nodes are private and can not be seen)
255 255 def render_project_nested_lists(projects)
256 256 s = ''
257 257 if projects.any?
258 258 ancestors = []
259 259 original_project = @project
260 260 projects.sort_by(&:lft).each do |project|
261 261 # set the project environment to please macros.
262 262 @project = project
263 263 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
264 264 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
265 265 else
266 266 ancestors.pop
267 267 s << "</li>"
268 268 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
269 269 ancestors.pop
270 270 s << "</ul></li>\n"
271 271 end
272 272 end
273 273 classes = (ancestors.empty? ? 'root' : 'child')
274 274 s << "<li class='#{classes}'><div class='#{classes}'>"
275 275 s << h(block_given? ? yield(project) : project.name)
276 276 s << "</div>\n"
277 277 ancestors << project
278 278 end
279 279 s << ("</li></ul>\n" * ancestors.size)
280 280 @project = original_project
281 281 end
282 282 s.html_safe
283 283 end
284 284
285 285 def render_page_hierarchy(pages, node=nil, options={})
286 286 content = ''
287 287 if pages[node]
288 288 content << "<ul class=\"pages-hierarchy\">\n"
289 289 pages[node].each do |page|
290 290 content << "<li>"
291 291 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
292 292 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
293 293 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
294 294 content << "</li>\n"
295 295 end
296 296 content << "</ul>\n"
297 297 end
298 298 content.html_safe
299 299 end
300 300
301 301 # Renders flash messages
302 302 def render_flash_messages
303 303 s = ''
304 304 flash.each do |k,v|
305 305 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
306 306 end
307 307 s.html_safe
308 308 end
309 309
310 310 # Renders tabs and their content
311 311 def render_tabs(tabs)
312 312 if tabs.any?
313 313 render :partial => 'common/tabs', :locals => {:tabs => tabs}
314 314 else
315 315 content_tag 'p', l(:label_no_data), :class => "nodata"
316 316 end
317 317 end
318 318
319 319 # Renders the project quick-jump box
320 320 def render_project_jump_box
321 321 return unless User.current.logged?
322 322 projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
323 323 if projects.any?
324 324 options =
325 325 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
326 326 '<option value="" disabled="disabled">---</option>').html_safe
327 327
328 328 options << project_tree_options_for_select(projects, :selected => @project) do |p|
329 329 { :value => project_path(:id => p, :jump => current_menu_item) }
330 330 end
331 331
332 332 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
333 333 end
334 334 end
335 335
336 336 def project_tree_options_for_select(projects, options = {})
337 337 s = ''
338 338 project_tree(projects) do |project, level|
339 339 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
340 340 tag_options = {:value => project.id}
341 341 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
342 342 tag_options[:selected] = 'selected'
343 343 else
344 344 tag_options[:selected] = nil
345 345 end
346 346 tag_options.merge!(yield(project)) if block_given?
347 347 s << content_tag('option', name_prefix + h(project), tag_options)
348 348 end
349 349 s.html_safe
350 350 end
351 351
352 352 # Yields the given block for each project with its level in the tree
353 353 #
354 354 # Wrapper for Project#project_tree
355 355 def project_tree(projects, &block)
356 356 Project.project_tree(projects, &block)
357 357 end
358 358
359 359 def principals_check_box_tags(name, principals)
360 360 s = ''
361 361 principals.each do |principal|
362 362 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
363 363 end
364 364 s.html_safe
365 365 end
366 366
367 367 # Returns a string for users/groups option tags
368 368 def principals_options_for_select(collection, selected=nil)
369 369 s = ''
370 370 if collection.include?(User.current)
371 371 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
372 372 end
373 373 groups = ''
374 374 collection.sort.each do |element|
375 375 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
376 376 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
377 377 end
378 378 unless groups.empty?
379 379 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
380 380 end
381 381 s.html_safe
382 382 end
383 383
384 384 # Options for the new membership projects combo-box
385 385 def options_for_membership_project_select(principal, projects)
386 386 options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
387 387 options << project_tree_options_for_select(projects) do |p|
388 388 {:disabled => principal.projects.to_a.include?(p)}
389 389 end
390 390 options
391 391 end
392 392
393 393 def option_tag(name, text, value, selected=nil, options={})
394 394 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
395 395 end
396 396
397 397 # Truncates and returns the string as a single line
398 398 def truncate_single_line(string, *args)
399 399 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
400 400 end
401 401
402 402 # Truncates at line break after 250 characters or options[:length]
403 403 def truncate_lines(string, options={})
404 404 length = options[:length] || 250
405 405 if string.to_s =~ /\A(.{#{length}}.*?)$/m
406 406 "#{$1}..."
407 407 else
408 408 string
409 409 end
410 410 end
411 411
412 412 def anchor(text)
413 413 text.to_s.gsub(' ', '_')
414 414 end
415 415
416 416 def html_hours(text)
417 417 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
418 418 end
419 419
420 420 def authoring(created, author, options={})
421 421 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
422 422 end
423 423
424 424 def time_tag(time)
425 425 text = distance_of_time_in_words(Time.now, time)
426 426 if @project
427 427 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
428 428 else
429 429 content_tag('abbr', text, :title => format_time(time))
430 430 end
431 431 end
432 432
433 433 def syntax_highlight_lines(name, content)
434 434 lines = []
435 435 syntax_highlight(name, content).each_line { |line| lines << line }
436 436 lines
437 437 end
438 438
439 439 def syntax_highlight(name, content)
440 440 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
441 441 end
442 442
443 443 def to_path_param(path)
444 444 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
445 445 str.blank? ? nil : str
446 446 end
447 447
448 448 def reorder_links(name, url, method = :post)
449 449 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
450 450 url.merge({"#{name}[move_to]" => 'highest'}),
451 451 :method => method, :title => l(:label_sort_highest)) +
452 452 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
453 453 url.merge({"#{name}[move_to]" => 'higher'}),
454 454 :method => method, :title => l(:label_sort_higher)) +
455 455 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
456 456 url.merge({"#{name}[move_to]" => 'lower'}),
457 457 :method => method, :title => l(:label_sort_lower)) +
458 458 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
459 459 url.merge({"#{name}[move_to]" => 'lowest'}),
460 460 :method => method, :title => l(:label_sort_lowest))
461 461 end
462 462
463 463 def breadcrumb(*args)
464 464 elements = args.flatten
465 465 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
466 466 end
467 467
468 468 def other_formats_links(&block)
469 469 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
470 470 yield Redmine::Views::OtherFormatsBuilder.new(self)
471 471 concat('</p>'.html_safe)
472 472 end
473 473
474 474 def page_header_title
475 475 if @project.nil? || @project.new_record?
476 476 h(Setting.app_title)
477 477 else
478 478 b = []
479 479 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
480 480 if ancestors.any?
481 481 root = ancestors.shift
482 482 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
483 483 if ancestors.size > 2
484 484 b << "\xe2\x80\xa6"
485 485 ancestors = ancestors[-2, 2]
486 486 end
487 487 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
488 488 end
489 489 b << h(@project)
490 490 b.join(" \xc2\xbb ").html_safe
491 491 end
492 492 end
493 493
494 494 # Returns a h2 tag and sets the html title with the given arguments
495 495 def title(*args)
496 496 strings = args.map do |arg|
497 497 if arg.is_a?(Array) && arg.size >= 2
498 498 link_to(*arg)
499 499 else
500 500 h(arg.to_s)
501 501 end
502 502 end
503 503 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
504 504 content_tag('h2', strings.join(' &#187; ').html_safe)
505 505 end
506 506
507 507 # Sets the html title
508 508 # Returns the html title when called without arguments
509 509 # Current project name and app_title and automatically appended
510 510 # Exemples:
511 511 # html_title 'Foo', 'Bar'
512 512 # html_title # => 'Foo - Bar - My Project - Redmine'
513 513 def html_title(*args)
514 514 if args.empty?
515 515 title = @html_title || []
516 516 title << @project.name if @project
517 517 title << Setting.app_title unless Setting.app_title == title.last
518 518 title.reject(&:blank?).join(' - ')
519 519 else
520 520 @html_title ||= []
521 521 @html_title += args
522 522 end
523 523 end
524 524
525 525 # Returns the theme, controller name, and action as css classes for the
526 526 # HTML body.
527 527 def body_css_classes
528 528 css = []
529 529 if theme = Redmine::Themes.theme(Setting.ui_theme)
530 530 css << 'theme-' + theme.name
531 531 end
532 532
533 533 css << 'project-' + @project.identifier if @project && @project.identifier.present?
534 534 css << 'controller-' + controller_name
535 535 css << 'action-' + action_name
536 536 css.join(' ')
537 537 end
538 538
539 539 def accesskey(s)
540 540 @used_accesskeys ||= []
541 541 key = Redmine::AccessKeys.key_for(s)
542 542 return nil if @used_accesskeys.include?(key)
543 543 @used_accesskeys << key
544 544 key
545 545 end
546 546
547 547 # Formats text according to system settings.
548 548 # 2 ways to call this method:
549 549 # * with a String: textilizable(text, options)
550 550 # * with an object and one of its attribute: textilizable(issue, :description, options)
551 551 def textilizable(*args)
552 552 options = args.last.is_a?(Hash) ? args.pop : {}
553 553 case args.size
554 554 when 1
555 555 obj = options[:object]
556 556 text = args.shift
557 557 when 2
558 558 obj = args.shift
559 559 attr = args.shift
560 560 text = obj.send(attr).to_s
561 561 else
562 562 raise ArgumentError, 'invalid arguments to textilizable'
563 563 end
564 564 return '' if text.blank?
565 565 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
566 566 only_path = options.delete(:only_path) == false ? false : true
567 567
568 568 text = text.dup
569 569 macros = catch_macros(text)
570 570 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
571 571
572 572 @parsed_headings = []
573 573 @heading_anchors = {}
574 574 @current_section = 0 if options[:edit_section_links]
575 575
576 576 parse_sections(text, project, obj, attr, only_path, options)
577 577 text = parse_non_pre_blocks(text, obj, macros) do |text|
578 578 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
579 579 send method_name, text, project, obj, attr, only_path, options
580 580 end
581 581 end
582 582 parse_headings(text, project, obj, attr, only_path, options)
583 583
584 584 if @parsed_headings.any?
585 585 replace_toc(text, @parsed_headings)
586 586 end
587 587
588 588 text.html_safe
589 589 end
590 590
591 591 def parse_non_pre_blocks(text, obj, macros)
592 592 s = StringScanner.new(text)
593 593 tags = []
594 594 parsed = ''
595 595 while !s.eos?
596 596 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
597 597 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
598 598 if tags.empty?
599 599 yield text
600 600 inject_macros(text, obj, macros) if macros.any?
601 601 else
602 602 inject_macros(text, obj, macros, false) if macros.any?
603 603 end
604 604 parsed << text
605 605 if tag
606 606 if closing
607 607 if tags.last == tag.downcase
608 608 tags.pop
609 609 end
610 610 else
611 611 tags << tag.downcase
612 612 end
613 613 parsed << full_tag
614 614 end
615 615 end
616 616 # Close any non closing tags
617 617 while tag = tags.pop
618 618 parsed << "</#{tag}>"
619 619 end
620 620 parsed
621 621 end
622 622
623 623 def parse_inline_attachments(text, project, obj, attr, only_path, options)
624 624 # when using an image link, try to use an attachment, if possible
625 625 attachments = options[:attachments] || []
626 626 attachments += obj.attachments if obj.respond_to?(:attachments)
627 627 if attachments.present?
628 628 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
629 629 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
630 630 # search for the picture in attachments
631 631 if found = Attachment.latest_attach(attachments, filename)
632 632 image_url = download_named_attachment_path(found, found.filename, :only_path => only_path)
633 633 desc = found.description.to_s.gsub('"', '')
634 634 if !desc.blank? && alttext.blank?
635 635 alt = " title=\"#{desc}\" alt=\"#{desc}\""
636 636 end
637 637 "src=\"#{image_url}\"#{alt}"
638 638 else
639 639 m
640 640 end
641 641 end
642 642 end
643 643 end
644 644
645 645 # Wiki links
646 646 #
647 647 # Examples:
648 648 # [[mypage]]
649 649 # [[mypage|mytext]]
650 650 # wiki links can refer other project wikis, using project name or identifier:
651 651 # [[project:]] -> wiki starting page
652 652 # [[project:|mytext]]
653 653 # [[project:mypage]]
654 654 # [[project:mypage|mytext]]
655 655 def parse_wiki_links(text, project, obj, attr, only_path, options)
656 656 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
657 657 link_project = project
658 658 esc, all, page, title = $1, $2, $3, $5
659 659 if esc.nil?
660 660 if page =~ /^([^\:]+)\:(.*)$/
661 661 identifier, page = $1, $2
662 662 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
663 663 title ||= identifier if page.blank?
664 664 end
665 665
666 666 if link_project && link_project.wiki
667 667 # extract anchor
668 668 anchor = nil
669 669 if page =~ /^(.+?)\#(.+)$/
670 670 page, anchor = $1, $2
671 671 end
672 672 anchor = sanitize_anchor_name(anchor) if anchor.present?
673 673 # check if page exists
674 674 wiki_page = link_project.wiki.find_page(page)
675 675 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
676 676 "##{anchor}"
677 677 else
678 678 case options[:wiki_links]
679 679 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
680 680 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
681 681 else
682 682 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
683 683 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
684 684 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
685 685 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
686 686 end
687 687 end
688 688 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
689 689 else
690 690 # project or wiki doesn't exist
691 691 all
692 692 end
693 693 else
694 694 all
695 695 end
696 696 end
697 697 end
698 698
699 699 # Redmine links
700 700 #
701 701 # Examples:
702 702 # Issues:
703 703 # #52 -> Link to issue #52
704 704 # Changesets:
705 705 # r52 -> Link to revision 52
706 706 # commit:a85130f -> Link to scmid starting with a85130f
707 707 # Documents:
708 708 # document#17 -> Link to document with id 17
709 709 # document:Greetings -> Link to the document with title "Greetings"
710 710 # document:"Some document" -> Link to the document with title "Some document"
711 711 # Versions:
712 712 # version#3 -> Link to version with id 3
713 713 # version:1.0.0 -> Link to version named "1.0.0"
714 714 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
715 715 # Attachments:
716 716 # attachment:file.zip -> Link to the attachment of the current object named file.zip
717 717 # Source files:
718 718 # source:some/file -> Link to the file located at /some/file in the project's repository
719 719 # source:some/file@52 -> Link to the file's revision 52
720 720 # source:some/file#L120 -> Link to line 120 of the file
721 721 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
722 722 # export:some/file -> Force the download of the file
723 723 # Forum messages:
724 724 # message#1218 -> Link to message with id 1218
725 725 # Projects:
726 726 # project:someproject -> Link to project named "someproject"
727 727 # project#3 -> Link to project with id 3
728 728 #
729 729 # Links can refer other objects from other projects, using project identifier:
730 730 # identifier:r52
731 731 # identifier:document:"Some document"
732 732 # identifier:version:1.0.0
733 733 # identifier:source:some/file
734 734 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
735 735 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|
736 736 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
737 737 link = nil
738 738 project = default_project
739 739 if project_identifier
740 740 project = Project.visible.find_by_identifier(project_identifier)
741 741 end
742 742 if esc.nil?
743 743 if prefix.nil? && sep == 'r'
744 744 if project
745 745 repository = nil
746 746 if repo_identifier
747 747 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
748 748 else
749 749 repository = project.repository
750 750 end
751 751 # project.changesets.visible raises an SQL error because of a double join on repositories
752 752 if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier))
753 753 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},
754 754 :class => 'changeset',
755 755 :title => truncate_single_line(changeset.comments, :length => 100))
756 756 end
757 757 end
758 758 elsif sep == '#'
759 759 oid = identifier.to_i
760 760 case prefix
761 761 when nil
762 762 if oid.to_s == identifier &&
763 763 issue = Issue.visible.includes(:status).find_by_id(oid)
764 764 anchor = comment_id ? "note-#{comment_id}" : nil
765 765 link = link_to(h("##{oid}#{comment_suffix}"), {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor},
766 766 :class => issue.css_classes,
767 767 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
768 768 end
769 769 when 'document'
770 770 if document = Document.visible.find_by_id(oid)
771 771 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
772 772 :class => 'document'
773 773 end
774 774 when 'version'
775 775 if version = Version.visible.find_by_id(oid)
776 776 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
777 777 :class => 'version'
778 778 end
779 779 when 'message'
780 780 if message = Message.visible.includes(:parent).find_by_id(oid)
781 781 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
782 782 end
783 783 when 'forum'
784 784 if board = Board.visible.find_by_id(oid)
785 785 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
786 786 :class => 'board'
787 787 end
788 788 when 'news'
789 789 if news = News.visible.find_by_id(oid)
790 790 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
791 791 :class => 'news'
792 792 end
793 793 when 'project'
794 794 if p = Project.visible.find_by_id(oid)
795 795 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
796 796 end
797 797 end
798 798 elsif sep == ':'
799 799 # removes the double quotes if any
800 800 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
801 801 case prefix
802 802 when 'document'
803 803 if project && document = project.documents.visible.find_by_title(name)
804 804 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
805 805 :class => 'document'
806 806 end
807 807 when 'version'
808 808 if project && version = project.versions.visible.find_by_name(name)
809 809 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
810 810 :class => 'version'
811 811 end
812 812 when 'forum'
813 813 if project && board = project.boards.visible.find_by_name(name)
814 814 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
815 815 :class => 'board'
816 816 end
817 817 when 'news'
818 818 if project && news = project.news.visible.find_by_title(name)
819 819 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
820 820 :class => 'news'
821 821 end
822 822 when 'commit', 'source', 'export'
823 823 if project
824 824 repository = nil
825 825 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
826 826 repo_prefix, repo_identifier, name = $1, $2, $3
827 827 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
828 828 else
829 829 repository = project.repository
830 830 end
831 831 if prefix == 'commit'
832 832 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
833 833 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},
834 834 :class => 'changeset',
835 835 :title => truncate_single_line(changeset.comments, :length => 100)
836 836 end
837 837 else
838 838 if repository && User.current.allowed_to?(:browse_repository, project)
839 839 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
840 840 path, rev, anchor = $1, $3, $5
841 841 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
842 842 :path => to_path_param(path),
843 843 :rev => rev,
844 844 :anchor => anchor},
845 845 :class => (prefix == 'export' ? 'source download' : 'source')
846 846 end
847 847 end
848 848 repo_prefix = nil
849 849 end
850 850 when 'attachment'
851 851 attachments = options[:attachments] || []
852 852 attachments += obj.attachments if obj.respond_to?(:attachments)
853 853 if attachments && attachment = Attachment.latest_attach(attachments, name)
854 854 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
855 855 end
856 856 when 'project'
857 857 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
858 858 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
859 859 end
860 860 end
861 861 end
862 862 end
863 863 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
864 864 end
865 865 end
866 866
867 867 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
868 868
869 869 def parse_sections(text, project, obj, attr, only_path, options)
870 870 return unless options[:edit_section_links]
871 871 text.gsub!(HEADING_RE) do
872 872 heading = $1
873 873 @current_section += 1
874 874 if @current_section > 1
875 875 content_tag('div',
876 876 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
877 877 :class => 'contextual',
878 878 :title => l(:button_edit_section),
879 879 :id => "section-#{@current_section}") + heading.html_safe
880 880 else
881 881 heading
882 882 end
883 883 end
884 884 end
885 885
886 886 # Headings and TOC
887 887 # Adds ids and links to headings unless options[:headings] is set to false
888 888 def parse_headings(text, project, obj, attr, only_path, options)
889 889 return if options[:headings] == false
890 890
891 891 text.gsub!(HEADING_RE) do
892 892 level, attrs, content = $2.to_i, $3, $4
893 893 item = strip_tags(content).strip
894 894 anchor = sanitize_anchor_name(item)
895 895 # used for single-file wiki export
896 896 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
897 897 @heading_anchors[anchor] ||= 0
898 898 idx = (@heading_anchors[anchor] += 1)
899 899 if idx > 1
900 900 anchor = "#{anchor}-#{idx}"
901 901 end
902 902 @parsed_headings << [level, anchor, item]
903 903 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
904 904 end
905 905 end
906 906
907 907 MACROS_RE = /(
908 908 (!)? # escaping
909 909 (
910 910 \{\{ # opening tag
911 911 ([\w]+) # macro name
912 912 (\(([^\n\r]*?)\))? # optional arguments
913 913 ([\n\r].*?[\n\r])? # optional block of text
914 914 \}\} # closing tag
915 915 )
916 916 )/mx unless const_defined?(:MACROS_RE)
917 917
918 918 MACRO_SUB_RE = /(
919 919 \{\{
920 920 macro\((\d+)\)
921 921 \}\}
922 922 )/x unless const_defined?(:MACRO_SUB_RE)
923 923
924 924 # Extracts macros from text
925 925 def catch_macros(text)
926 926 macros = {}
927 927 text.gsub!(MACROS_RE) do
928 928 all, macro = $1, $4.downcase
929 929 if macro_exists?(macro) || all =~ MACRO_SUB_RE
930 930 index = macros.size
931 931 macros[index] = all
932 932 "{{macro(#{index})}}"
933 933 else
934 934 all
935 935 end
936 936 end
937 937 macros
938 938 end
939 939
940 940 # Executes and replaces macros in text
941 941 def inject_macros(text, obj, macros, execute=true)
942 942 text.gsub!(MACRO_SUB_RE) do
943 943 all, index = $1, $2.to_i
944 944 orig = macros.delete(index)
945 945 if execute && orig && orig =~ MACROS_RE
946 946 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
947 947 if esc.nil?
948 948 h(exec_macro(macro, obj, args, block) || all)
949 949 else
950 950 h(all)
951 951 end
952 952 elsif orig
953 953 h(orig)
954 954 else
955 955 h(all)
956 956 end
957 957 end
958 958 end
959 959
960 960 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
961 961
962 962 # Renders the TOC with given headings
963 963 def replace_toc(text, headings)
964 964 text.gsub!(TOC_RE) do
965 965 # Keep only the 4 first levels
966 966 headings = headings.select{|level, anchor, item| level <= 4}
967 967 if headings.empty?
968 968 ''
969 969 else
970 970 div_class = 'toc'
971 971 div_class << ' right' if $1 == '>'
972 972 div_class << ' left' if $1 == '<'
973 973 out = "<ul class=\"#{div_class}\"><li>"
974 974 root = headings.map(&:first).min
975 975 current = root
976 976 started = false
977 977 headings.each do |level, anchor, item|
978 978 if level > current
979 979 out << '<ul><li>' * (level - current)
980 980 elsif level < current
981 981 out << "</li></ul>\n" * (current - level) + "</li><li>"
982 982 elsif started
983 983 out << '</li><li>'
984 984 end
985 985 out << "<a href=\"##{anchor}\">#{item}</a>"
986 986 current = level
987 987 started = true
988 988 end
989 989 out << '</li></ul>' * (current - root)
990 990 out << '</li></ul>'
991 991 end
992 992 end
993 993 end
994 994
995 995 # Same as Rails' simple_format helper without using paragraphs
996 996 def simple_format_without_paragraph(text)
997 997 text.to_s.
998 998 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
999 999 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1000 1000 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1001 1001 html_safe
1002 1002 end
1003 1003
1004 1004 def lang_options_for_select(blank=true)
1005 1005 (blank ? [["(auto)", ""]] : []) + languages_options
1006 1006 end
1007 1007
1008 1008 def label_tag_for(name, option_tags = nil, options = {})
1009 1009 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
1010 1010 content_tag("label", label_text)
1011 1011 end
1012 1012
1013 1013 def labelled_form_for(*args, &proc)
1014 1014 args << {} unless args.last.is_a?(Hash)
1015 1015 options = args.last
1016 1016 if args.first.is_a?(Symbol)
1017 1017 options.merge!(:as => args.shift)
1018 1018 end
1019 1019 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1020 1020 form_for(*args, &proc)
1021 1021 end
1022 1022
1023 1023 def labelled_fields_for(*args, &proc)
1024 1024 args << {} unless args.last.is_a?(Hash)
1025 1025 options = args.last
1026 1026 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1027 1027 fields_for(*args, &proc)
1028 1028 end
1029 1029
1030 1030 def labelled_remote_form_for(*args, &proc)
1031 1031 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_remote_form_for is deprecated and will be removed in Redmine 2.2."
1032 1032 args << {} unless args.last.is_a?(Hash)
1033 1033 options = args.last
1034 1034 options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
1035 1035 form_for(*args, &proc)
1036 1036 end
1037 1037
1038 1038 def error_messages_for(*objects)
1039 1039 html = ""
1040 1040 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1041 1041 errors = objects.map {|o| o.errors.full_messages}.flatten
1042 1042 if errors.any?
1043 1043 html << "<div id='errorExplanation'><ul>\n"
1044 1044 errors.each do |error|
1045 1045 html << "<li>#{h error}</li>\n"
1046 1046 end
1047 1047 html << "</ul></div>\n"
1048 1048 end
1049 1049 html.html_safe
1050 1050 end
1051 1051
1052 1052 def delete_link(url, options={})
1053 1053 options = {
1054 1054 :method => :delete,
1055 1055 :data => {:confirm => l(:text_are_you_sure)},
1056 1056 :class => 'icon icon-del'
1057 1057 }.merge(options)
1058 1058
1059 1059 link_to l(:button_delete), url, options
1060 1060 end
1061 1061
1062 1062 def preview_link(url, form, target='preview', options={})
1063 1063 content_tag 'a', l(:label_preview), {
1064 1064 :href => "#",
1065 1065 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1066 1066 :accesskey => accesskey(:preview)
1067 1067 }.merge(options)
1068 1068 end
1069 1069
1070 1070 def link_to_function(name, function, html_options={})
1071 1071 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1072 1072 end
1073 1073
1074 1074 # Helper to render JSON in views
1075 1075 def raw_json(arg)
1076 1076 arg.to_json.to_s.gsub('/', '\/').html_safe
1077 1077 end
1078 1078
1079 1079 def back_url
1080 1080 url = params[:back_url]
1081 1081 if url.nil? && referer = request.env['HTTP_REFERER']
1082 1082 url = CGI.unescape(referer.to_s)
1083 1083 end
1084 1084 url
1085 1085 end
1086 1086
1087 1087 def back_url_hidden_field_tag
1088 1088 url = back_url
1089 1089 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1090 1090 end
1091 1091
1092 1092 def check_all_links(form_name)
1093 1093 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1094 1094 " | ".html_safe +
1095 1095 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1096 1096 end
1097 1097
1098 1098 def progress_bar(pcts, options={})
1099 1099 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1100 1100 pcts = pcts.collect(&:round)
1101 1101 pcts[1] = pcts[1] - pcts[0]
1102 1102 pcts << (100 - pcts[1] - pcts[0])
1103 1103 width = options[:width] || '100px;'
1104 1104 legend = options[:legend] || ''
1105 1105 content_tag('table',
1106 1106 content_tag('tr',
1107 1107 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1108 1108 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1109 1109 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1110 1110 ), :class => "progress progress-#{pcts[0]}", :style => "width: #{width};").html_safe +
1111 1111 content_tag('p', legend, :class => 'percent').html_safe
1112 1112 end
1113 1113
1114 1114 def checked_image(checked=true)
1115 1115 if checked
1116 1116 image_tag 'toggle_check.png'
1117 1117 end
1118 1118 end
1119 1119
1120 1120 def context_menu(url)
1121 1121 unless @context_menu_included
1122 1122 content_for :header_tags do
1123 1123 javascript_include_tag('context_menu') +
1124 1124 stylesheet_link_tag('context_menu')
1125 1125 end
1126 1126 if l(:direction) == 'rtl'
1127 1127 content_for :header_tags do
1128 1128 stylesheet_link_tag('context_menu_rtl')
1129 1129 end
1130 1130 end
1131 1131 @context_menu_included = true
1132 1132 end
1133 1133 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1134 1134 end
1135 1135
1136 1136 def calendar_for(field_id)
1137 1137 include_calendar_headers_tags
1138 1138 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1139 1139 end
1140 1140
1141 1141 def include_calendar_headers_tags
1142 1142 unless @calendar_headers_tags_included
1143 1143 tags = javascript_include_tag("datepicker")
1144 1144 @calendar_headers_tags_included = true
1145 1145 content_for :header_tags do
1146 1146 start_of_week = Setting.start_of_week
1147 1147 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1148 1148 # Redmine uses 1..7 (monday..sunday) in settings and locales
1149 1149 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1150 1150 start_of_week = start_of_week.to_i % 7
1151 1151 tags << javascript_tag(
1152 1152 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1153 1153 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1154 1154 path_to_image('/images/calendar.png') +
1155 1155 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1156 1156 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1157 1157 "beforeShow: beforeShowDatePicker};")
1158 1158 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1159 1159 unless jquery_locale == 'en'
1160 1160 tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js")
1161 1161 end
1162 1162 tags
1163 1163 end
1164 1164 end
1165 1165 end
1166 1166
1167 1167 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1168 1168 # Examples:
1169 1169 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1170 1170 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1171 1171 #
1172 1172 def stylesheet_link_tag(*sources)
1173 1173 options = sources.last.is_a?(Hash) ? sources.pop : {}
1174 1174 plugin = options.delete(:plugin)
1175 1175 sources = sources.map do |source|
1176 1176 if plugin
1177 1177 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1178 1178 elsif current_theme && current_theme.stylesheets.include?(source)
1179 1179 current_theme.stylesheet_path(source)
1180 1180 else
1181 1181 source
1182 1182 end
1183 1183 end
1184 1184 super sources, options
1185 1185 end
1186 1186
1187 1187 # Overrides Rails' image_tag with themes and plugins support.
1188 1188 # Examples:
1189 1189 # image_tag('image.png') # => picks image.png from the current theme or defaults
1190 1190 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1191 1191 #
1192 1192 def image_tag(source, options={})
1193 1193 if plugin = options.delete(:plugin)
1194 1194 source = "/plugin_assets/#{plugin}/images/#{source}"
1195 1195 elsif current_theme && current_theme.images.include?(source)
1196 1196 source = current_theme.image_path(source)
1197 1197 end
1198 1198 super source, options
1199 1199 end
1200 1200
1201 1201 # Overrides Rails' javascript_include_tag with plugins support
1202 1202 # Examples:
1203 1203 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1204 1204 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1205 1205 #
1206 1206 def javascript_include_tag(*sources)
1207 1207 options = sources.last.is_a?(Hash) ? sources.pop : {}
1208 1208 if plugin = options.delete(:plugin)
1209 1209 sources = sources.map do |source|
1210 1210 if plugin
1211 1211 "/plugin_assets/#{plugin}/javascripts/#{source}"
1212 1212 else
1213 1213 source
1214 1214 end
1215 1215 end
1216 1216 end
1217 1217 super sources, options
1218 1218 end
1219 1219
1220 1220 # TODO: remove this in 2.5.0
1221 1221 def has_content?(name)
1222 1222 content_for?(name)
1223 1223 end
1224 1224
1225 1225 def sidebar_content?
1226 1226 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1227 1227 end
1228 1228
1229 1229 def view_layouts_base_sidebar_hook_response
1230 1230 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1231 1231 end
1232 1232
1233 1233 def email_delivery_enabled?
1234 1234 !!ActionMailer::Base.perform_deliveries
1235 1235 end
1236 1236
1237 1237 # Returns the avatar image tag for the given +user+ if avatars are enabled
1238 1238 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1239 1239 def avatar(user, options = { })
1240 1240 if Setting.gravatar_enabled?
1241 1241 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1242 1242 email = nil
1243 1243 if user.respond_to?(:mail)
1244 1244 email = user.mail
1245 1245 elsif user.to_s =~ %r{<(.+?)>}
1246 1246 email = $1
1247 1247 end
1248 1248 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1249 1249 else
1250 1250 ''
1251 1251 end
1252 1252 end
1253 1253
1254 1254 def sanitize_anchor_name(anchor)
1255 1255 if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java'
1256 1256 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1257 1257 else
1258 1258 # TODO: remove when ruby1.8 is no longer supported
1259 1259 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1260 1260 end
1261 1261 end
1262 1262
1263 1263 # Returns the javascript tags that are included in the html layout head
1264 1264 def javascript_heads
1265 1265 tags = javascript_include_tag('jquery-1.8.3-ui-1.9.2-ujs-2.0.3', 'application')
1266 1266 unless User.current.pref.warn_on_leaving_unsaved == '0'
1267 1267 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1268 1268 end
1269 1269 tags
1270 1270 end
1271 1271
1272 1272 def favicon
1273 fav_path = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1274 "<link rel='shortcut icon' href='#{image_path(fav_path)}' />".html_safe
1273 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1274 end
1275
1276 # Returns the path to the favicon
1277 def favicon_path
1278 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1279 image_path(icon)
1280 end
1281
1282 # Returns the full URL to the favicon
1283 def favicon_url
1284 # TODO: use #image_url introduced in Rails4
1285 path = favicon_path
1286 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1287 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1275 1288 end
1276 1289
1277 1290 def robot_exclusion_tag
1278 1291 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1279 1292 end
1280 1293
1281 1294 # Returns true if arg is expected in the API response
1282 1295 def include_in_api_response?(arg)
1283 1296 unless @included_in_api_response
1284 1297 param = params[:include]
1285 1298 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1286 1299 @included_in_api_response.collect!(&:strip)
1287 1300 end
1288 1301 @included_in_api_response.include?(arg.to_s)
1289 1302 end
1290 1303
1291 1304 # Returns options or nil if nometa param or X-Redmine-Nometa header
1292 1305 # was set in the request
1293 1306 def api_meta(options)
1294 1307 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1295 1308 # compatibility mode for activeresource clients that raise
1296 1309 # an error when unserializing an array with attributes
1297 1310 nil
1298 1311 else
1299 1312 options
1300 1313 end
1301 1314 end
1302 1315
1303 1316 private
1304 1317
1305 1318 def wiki_helper
1306 1319 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1307 1320 extend helper
1308 1321 return self
1309 1322 end
1310 1323
1311 1324 def link_to_content_update(text, url_params = {}, html_options = {})
1312 1325 link_to(text, url_params, html_options)
1313 1326 end
1314 1327 end
@@ -1,1267 +1,1289
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" id="section-4" 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" id="section-5" 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.where(:id => 1).update_all(:identifier => 25)
1152 1152 assert_equal '<a href="/projects/1">eCookbook</a>',
1153 1153 link_to_project(Project.find(1))
1154 1154 end
1155 1155
1156 1156 def test_principals_options_for_select_with_users
1157 1157 User.current = nil
1158 1158 users = [User.find(2), User.find(4)]
1159 1159 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>),
1160 1160 principals_options_for_select(users)
1161 1161 end
1162 1162
1163 1163 def test_principals_options_for_select_with_selected
1164 1164 User.current = nil
1165 1165 users = [User.find(2), User.find(4)]
1166 1166 assert_equal %(<option value="2">John Smith</option><option value="4" selected="selected">Robert Hill</option>),
1167 1167 principals_options_for_select(users, User.find(4))
1168 1168 end
1169 1169
1170 1170 def test_principals_options_for_select_with_users_and_groups
1171 1171 User.current = nil
1172 1172 users = [User.find(2), Group.find(11), User.find(4), Group.find(10)]
1173 1173 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>) +
1174 1174 %(<optgroup label="Groups"><option value="10">A Team</option><option value="11">B Team</option></optgroup>),
1175 1175 principals_options_for_select(users)
1176 1176 end
1177 1177
1178 1178 def test_principals_options_for_select_with_empty_collection
1179 1179 assert_equal '', principals_options_for_select([])
1180 1180 end
1181 1181
1182 1182 def test_principals_options_for_select_should_include_me_option_when_current_user_is_in_collection
1183 1183 users = [User.find(2), User.find(4)]
1184 1184 User.current = User.find(4)
1185 1185 assert_include '<option value="4">&lt;&lt; me &gt;&gt;</option>', principals_options_for_select(users)
1186 1186 end
1187 1187
1188 1188 def test_stylesheet_link_tag_should_pick_the_default_stylesheet
1189 1189 assert_match 'href="/stylesheets/styles.css"', stylesheet_link_tag("styles")
1190 1190 end
1191 1191
1192 1192 def test_stylesheet_link_tag_for_plugin_should_pick_the_plugin_stylesheet
1193 1193 assert_match 'href="/plugin_assets/foo/stylesheets/styles.css"', stylesheet_link_tag("styles", :plugin => :foo)
1194 1194 end
1195 1195
1196 1196 def test_image_tag_should_pick_the_default_image
1197 1197 assert_match 'src="/images/image.png"', image_tag("image.png")
1198 1198 end
1199 1199
1200 1200 def test_image_tag_should_pick_the_theme_image_if_it_exists
1201 1201 theme = Redmine::Themes.themes.last
1202 1202 theme.images << 'image.png'
1203 1203
1204 1204 with_settings :ui_theme => theme.id do
1205 1205 assert_match %|src="/themes/#{theme.dir}/images/image.png"|, image_tag("image.png")
1206 1206 assert_match %|src="/images/other.png"|, image_tag("other.png")
1207 1207 end
1208 1208 ensure
1209 1209 theme.images.delete 'image.png'
1210 1210 end
1211 1211
1212 1212 def test_image_tag_sfor_plugin_should_pick_the_plugin_image
1213 1213 assert_match 'src="/plugin_assets/foo/images/image.png"', image_tag("image.png", :plugin => :foo)
1214 1214 end
1215 1215
1216 1216 def test_javascript_include_tag_should_pick_the_default_javascript
1217 1217 assert_match 'src="/javascripts/scripts.js"', javascript_include_tag("scripts")
1218 1218 end
1219 1219
1220 1220 def test_javascript_include_tag_for_plugin_should_pick_the_plugin_javascript
1221 1221 assert_match 'src="/plugin_assets/foo/javascripts/scripts.js"', javascript_include_tag("scripts", :plugin => :foo)
1222 1222 end
1223 1223
1224 1224 def test_raw_json_should_escape_closing_tags
1225 1225 s = raw_json(["<foo>bar</foo>"])
1226 1226 assert_equal '["<foo>bar<\/foo>"]', s
1227 1227 end
1228 1228
1229 1229 def test_raw_json_should_be_html_safe
1230 1230 s = raw_json(["foo"])
1231 1231 assert s.html_safe?
1232 1232 end
1233 1233
1234 1234 def test_html_title_should_app_title_if_not_set
1235 1235 assert_equal 'Redmine', html_title
1236 1236 end
1237 1237
1238 1238 def test_html_title_should_join_items
1239 1239 html_title 'Foo', 'Bar'
1240 1240 assert_equal 'Foo - Bar - Redmine', html_title
1241 1241 end
1242 1242
1243 1243 def test_html_title_should_append_current_project_name
1244 1244 @project = Project.find(1)
1245 1245 html_title 'Foo', 'Bar'
1246 1246 assert_equal 'Foo - Bar - eCookbook - Redmine', html_title
1247 1247 end
1248 1248
1249 1249 def test_title_should_return_a_h2_tag
1250 1250 assert_equal '<h2>Foo</h2>', title('Foo')
1251 1251 end
1252 1252
1253 1253 def test_title_should_set_html_title
1254 1254 title('Foo')
1255 1255 assert_equal 'Foo - Redmine', html_title
1256 1256 end
1257 1257
1258 1258 def test_title_should_turn_arrays_into_links
1259 1259 assert_equal '<h2><a href="/foo">Foo</a></h2>', title(['Foo', '/foo'])
1260 1260 assert_equal 'Foo - Redmine', html_title
1261 1261 end
1262 1262
1263 1263 def test_title_should_join_items
1264 1264 assert_equal '<h2>Foo &#187; Bar</h2>', title('Foo', 'Bar')
1265 1265 assert_equal 'Bar - Foo - Redmine', html_title
1266 1266 end
1267
1268 def test_favicon_path
1269 assert_match %r{^/favicon\.ico}, favicon_path
1270 end
1271
1272 def test_favicon_path_with_suburi
1273 Redmine::Utils.relative_url_root = '/foo'
1274 assert_match %r{^/foo/favicon\.ico}, favicon_path
1275 ensure
1276 Redmine::Utils.relative_url_root = ''
1277 end
1278
1279 def test_favicon_url
1280 assert_match %r{^http://test\.host/favicon\.ico}, favicon_url
1281 end
1282
1283 def test_favicon_url_with_suburi
1284 Redmine::Utils.relative_url_root = '/foo'
1285 assert_match %r{^http://test\.host/foo/favicon\.ico}, favicon_url
1286 ensure
1287 Redmine::Utils.relative_url_root = ''
1288 end
1267 1289 end
General Comments 0
You need to be logged in to leave comments. Login now