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