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