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