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