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