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