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