##// END OF EJS Templates
Adds a <<me>> option at the top of the assignee drop-down (#1102)....
Jean-Philippe Lang -
r8568:45e3dafa8fc7
parent child
Show More
@@ -1,1089 +1,1092
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2011 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
28 28 extend Forwardable
29 29 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
30 30
31 31 # Return true if user is authorized for controller/action, otherwise false
32 32 def authorize_for(controller, action)
33 33 User.current.allowed_to?({:controller => controller, :action => action}, @project)
34 34 end
35 35
36 36 # Display a link if user is authorized
37 37 #
38 38 # @param [String] name Anchor text (passed to link_to)
39 39 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
40 40 # @param [optional, Hash] html_options Options passed to link_to
41 41 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
42 42 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
43 43 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
44 44 end
45 45
46 46 # Display a link to remote if user is authorized
47 47 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
48 48 url = options[:url] || {}
49 49 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
50 50 end
51 51
52 52 # Displays a link to user's account page if active
53 53 def link_to_user(user, options={})
54 54 if user.is_a?(User)
55 55 name = h(user.name(options[:format]))
56 56 if user.active?
57 57 link_to name, :controller => 'users', :action => 'show', :id => user
58 58 else
59 59 name
60 60 end
61 61 else
62 62 h(user.to_s)
63 63 end
64 64 end
65 65
66 66 # Displays a link to +issue+ with its subject.
67 67 # Examples:
68 68 #
69 69 # link_to_issue(issue) # => Defect #6: This is the subject
70 70 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
71 71 # link_to_issue(issue, :subject => false) # => Defect #6
72 72 # link_to_issue(issue, :project => true) # => Foo - Defect #6
73 73 #
74 74 def link_to_issue(issue, options={})
75 75 title = nil
76 76 subject = nil
77 77 if options[:subject] == false
78 78 title = truncate(issue.subject, :length => 60)
79 79 else
80 80 subject = issue.subject
81 81 if options[:truncate]
82 82 subject = truncate(subject, :length => options[:truncate])
83 83 end
84 84 end
85 85 s = link_to "#{h(issue.tracker)} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
86 86 :class => issue.css_classes,
87 87 :title => title
88 88 s << h(": #{subject}") if subject
89 89 s = h("#{issue.project} - ") + s if options[:project]
90 90 s
91 91 end
92 92
93 93 # Generates a link to an attachment.
94 94 # Options:
95 95 # * :text - Link text (default to attachment filename)
96 96 # * :download - Force download (default: false)
97 97 def link_to_attachment(attachment, options={})
98 98 text = options.delete(:text) || attachment.filename
99 99 action = options.delete(:download) ? 'download' : 'show'
100 100 link_to(h(text),
101 101 {:controller => 'attachments', :action => action,
102 102 :id => attachment, :filename => attachment.filename },
103 103 options)
104 104 end
105 105
106 106 # Generates a link to a SCM revision
107 107 # Options:
108 108 # * :text - Link text (default to the formatted revision)
109 109 def link_to_revision(revision, repository, options={})
110 110 if repository.is_a?(Project)
111 111 repository = repository.repository
112 112 end
113 113 text = options.delete(:text) || format_revision(revision)
114 114 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
115 115 link_to(
116 116 h(text),
117 117 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
118 118 :title => l(:label_revision_id, format_revision(revision))
119 119 )
120 120 end
121 121
122 122 # Generates a link to a message
123 123 def link_to_message(message, options={}, html_options = nil)
124 124 link_to(
125 125 h(truncate(message.subject, :length => 60)),
126 126 { :controller => 'messages', :action => 'show',
127 127 :board_id => message.board_id,
128 128 :id => message.root,
129 129 :r => (message.parent_id && message.id),
130 130 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
131 131 }.merge(options),
132 132 html_options
133 133 )
134 134 end
135 135
136 136 # Generates a link to a project if active
137 137 # Examples:
138 138 #
139 139 # link_to_project(project) # => link to the specified project overview
140 140 # link_to_project(project, :action=>'settings') # => link to project settings
141 141 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
142 142 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
143 143 #
144 144 def link_to_project(project, options={}, html_options = nil)
145 145 if project.active?
146 146 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
147 147 link_to(h(project), url, html_options)
148 148 else
149 149 h(project)
150 150 end
151 151 end
152 152
153 153 def toggle_link(name, id, options={})
154 154 onclick = "Element.toggle('#{id}'); "
155 155 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
156 156 onclick << "return false;"
157 157 link_to(name, "#", :onclick => onclick)
158 158 end
159 159
160 160 def image_to_function(name, function, html_options = {})
161 161 html_options.symbolize_keys!
162 162 tag(:input, html_options.merge({
163 163 :type => "image", :src => image_path(name),
164 164 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
165 165 }))
166 166 end
167 167
168 168 def prompt_to_remote(name, text, param, url, html_options = {})
169 169 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
170 170 link_to name, {}, html_options
171 171 end
172 172
173 173 def format_activity_title(text)
174 174 h(truncate_single_line(text, :length => 100))
175 175 end
176 176
177 177 def format_activity_day(date)
178 178 date == Date.today ? l(:label_today).titleize : format_date(date)
179 179 end
180 180
181 181 def format_activity_description(text)
182 182 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
183 183 ).gsub(/[\r\n]+/, "<br />").html_safe
184 184 end
185 185
186 186 def format_version_name(version)
187 187 if version.project == @project
188 188 h(version)
189 189 else
190 190 h("#{version.project} - #{version}")
191 191 end
192 192 end
193 193
194 194 def due_date_distance_in_words(date)
195 195 if date
196 196 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
197 197 end
198 198 end
199 199
200 200 def render_page_hierarchy(pages, node=nil, options={})
201 201 content = ''
202 202 if pages[node]
203 203 content << "<ul class=\"pages-hierarchy\">\n"
204 204 pages[node].each do |page|
205 205 content << "<li>"
206 206 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
207 207 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
208 208 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
209 209 content << "</li>\n"
210 210 end
211 211 content << "</ul>\n"
212 212 end
213 213 content.html_safe
214 214 end
215 215
216 216 # Renders flash messages
217 217 def render_flash_messages
218 218 s = ''
219 219 flash.each do |k,v|
220 220 s << (content_tag('div', v.html_safe, :class => "flash #{k}"))
221 221 end
222 222 s.html_safe
223 223 end
224 224
225 225 # Renders tabs and their content
226 226 def render_tabs(tabs)
227 227 if tabs.any?
228 228 render :partial => 'common/tabs', :locals => {:tabs => tabs}
229 229 else
230 230 content_tag 'p', l(:label_no_data), :class => "nodata"
231 231 end
232 232 end
233 233
234 234 # Renders the project quick-jump box
235 235 def render_project_jump_box
236 236 return unless User.current.logged?
237 237 projects = User.current.memberships.collect(&:project).compact.uniq
238 238 if projects.any?
239 239 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
240 240 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
241 241 '<option value="" disabled="disabled">---</option>'
242 242 s << project_tree_options_for_select(projects, :selected => @project) do |p|
243 243 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
244 244 end
245 245 s << '</select>'
246 246 s.html_safe
247 247 end
248 248 end
249 249
250 250 def project_tree_options_for_select(projects, options = {})
251 251 s = ''
252 252 project_tree(projects) do |project, level|
253 253 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
254 254 tag_options = {:value => project.id}
255 255 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
256 256 tag_options[:selected] = 'selected'
257 257 else
258 258 tag_options[:selected] = nil
259 259 end
260 260 tag_options.merge!(yield(project)) if block_given?
261 261 s << content_tag('option', name_prefix + h(project), tag_options)
262 262 end
263 263 s.html_safe
264 264 end
265 265
266 266 # Yields the given block for each project with its level in the tree
267 267 #
268 268 # Wrapper for Project#project_tree
269 269 def project_tree(projects, &block)
270 270 Project.project_tree(projects, &block)
271 271 end
272 272
273 273 def project_nested_ul(projects, &block)
274 274 s = ''
275 275 if projects.any?
276 276 ancestors = []
277 277 projects.sort_by(&:lft).each do |project|
278 278 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
279 279 s << "<ul>\n"
280 280 else
281 281 ancestors.pop
282 282 s << "</li>"
283 283 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
284 284 ancestors.pop
285 285 s << "</ul></li>\n"
286 286 end
287 287 end
288 288 s << "<li>"
289 289 s << yield(project).to_s
290 290 ancestors << project
291 291 end
292 292 s << ("</li></ul>\n" * ancestors.size)
293 293 end
294 294 s.html_safe
295 295 end
296 296
297 297 def principals_check_box_tags(name, principals)
298 298 s = ''
299 299 principals.sort.each do |principal|
300 300 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
301 301 end
302 302 s.html_safe
303 303 end
304 304
305 305 # Returns a string for users/groups option tags
306 306 def principals_options_for_select(collection, selected=nil)
307 307 s = ''
308 if collection.include?(User.current)
309 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
310 end
308 311 groups = ''
309 312 collection.sort.each do |element|
310 313 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected)
311 314 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
312 315 end
313 316 unless groups.empty?
314 317 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
315 318 end
316 319 s
317 320 end
318 321
319 322 # Truncates and returns the string as a single line
320 323 def truncate_single_line(string, *args)
321 324 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
322 325 end
323 326
324 327 # Truncates at line break after 250 characters or options[:length]
325 328 def truncate_lines(string, options={})
326 329 length = options[:length] || 250
327 330 if string.to_s =~ /\A(.{#{length}}.*?)$/m
328 331 "#{$1}..."
329 332 else
330 333 string
331 334 end
332 335 end
333 336
334 337 def anchor(text)
335 338 text.to_s.gsub(' ', '_')
336 339 end
337 340
338 341 def html_hours(text)
339 342 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
340 343 end
341 344
342 345 def authoring(created, author, options={})
343 346 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
344 347 end
345 348
346 349 def time_tag(time)
347 350 text = distance_of_time_in_words(Time.now, time)
348 351 if @project
349 352 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
350 353 else
351 354 content_tag('acronym', text, :title => format_time(time))
352 355 end
353 356 end
354 357
355 358 def syntax_highlight(name, content)
356 359 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
357 360 end
358 361
359 362 def to_path_param(path)
360 363 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
361 364 end
362 365
363 366 def pagination_links_full(paginator, count=nil, options={})
364 367 page_param = options.delete(:page_param) || :page
365 368 per_page_links = options.delete(:per_page_links)
366 369 url_param = params.dup
367 370
368 371 html = ''
369 372 if paginator.current.previous
370 373 # \xc2\xab(utf-8) = &#171;
371 374 html << link_to_content_update(
372 375 "\xc2\xab " + l(:label_previous),
373 376 url_param.merge(page_param => paginator.current.previous)) + ' '
374 377 end
375 378
376 379 html << (pagination_links_each(paginator, options) do |n|
377 380 link_to_content_update(n.to_s, url_param.merge(page_param => n))
378 381 end || '')
379 382
380 383 if paginator.current.next
381 384 # \xc2\xbb(utf-8) = &#187;
382 385 html << ' ' + link_to_content_update(
383 386 (l(:label_next) + " \xc2\xbb"),
384 387 url_param.merge(page_param => paginator.current.next))
385 388 end
386 389
387 390 unless count.nil?
388 391 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
389 392 if per_page_links != false && links = per_page_links(paginator.items_per_page)
390 393 html << " | #{links}"
391 394 end
392 395 end
393 396
394 397 html.html_safe
395 398 end
396 399
397 400 def per_page_links(selected=nil)
398 401 links = Setting.per_page_options_array.collect do |n|
399 402 n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
400 403 end
401 404 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
402 405 end
403 406
404 407 def reorder_links(name, url, method = :post)
405 408 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
406 409 url.merge({"#{name}[move_to]" => 'highest'}),
407 410 :method => method, :title => l(:label_sort_highest)) +
408 411 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
409 412 url.merge({"#{name}[move_to]" => 'higher'}),
410 413 :method => method, :title => l(:label_sort_higher)) +
411 414 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
412 415 url.merge({"#{name}[move_to]" => 'lower'}),
413 416 :method => method, :title => l(:label_sort_lower)) +
414 417 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
415 418 url.merge({"#{name}[move_to]" => 'lowest'}),
416 419 :method => method, :title => l(:label_sort_lowest))
417 420 end
418 421
419 422 def breadcrumb(*args)
420 423 elements = args.flatten
421 424 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
422 425 end
423 426
424 427 def other_formats_links(&block)
425 428 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
426 429 yield Redmine::Views::OtherFormatsBuilder.new(self)
427 430 concat('</p>'.html_safe)
428 431 end
429 432
430 433 def page_header_title
431 434 if @project.nil? || @project.new_record?
432 435 h(Setting.app_title)
433 436 else
434 437 b = []
435 438 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
436 439 if ancestors.any?
437 440 root = ancestors.shift
438 441 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
439 442 if ancestors.size > 2
440 443 b << "\xe2\x80\xa6"
441 444 ancestors = ancestors[-2, 2]
442 445 end
443 446 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
444 447 end
445 448 b << h(@project)
446 449 b.join(" \xc2\xbb ").html_safe
447 450 end
448 451 end
449 452
450 453 def html_title(*args)
451 454 if args.empty?
452 455 title = @html_title || []
453 456 title << @project.name if @project
454 457 title << Setting.app_title unless Setting.app_title == title.last
455 458 title.select {|t| !t.blank? }.join(' - ')
456 459 else
457 460 @html_title ||= []
458 461 @html_title += args
459 462 end
460 463 end
461 464
462 465 # Returns the theme, controller name, and action as css classes for the
463 466 # HTML body.
464 467 def body_css_classes
465 468 css = []
466 469 if theme = Redmine::Themes.theme(Setting.ui_theme)
467 470 css << 'theme-' + theme.name
468 471 end
469 472
470 473 css << 'controller-' + params[:controller]
471 474 css << 'action-' + params[:action]
472 475 css.join(' ')
473 476 end
474 477
475 478 def accesskey(s)
476 479 Redmine::AccessKeys.key_for s
477 480 end
478 481
479 482 # Formats text according to system settings.
480 483 # 2 ways to call this method:
481 484 # * with a String: textilizable(text, options)
482 485 # * with an object and one of its attribute: textilizable(issue, :description, options)
483 486 def textilizable(*args)
484 487 options = args.last.is_a?(Hash) ? args.pop : {}
485 488 case args.size
486 489 when 1
487 490 obj = options[:object]
488 491 text = args.shift
489 492 when 2
490 493 obj = args.shift
491 494 attr = args.shift
492 495 text = obj.send(attr).to_s
493 496 else
494 497 raise ArgumentError, 'invalid arguments to textilizable'
495 498 end
496 499 return '' if text.blank?
497 500 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
498 501 only_path = options.delete(:only_path) == false ? false : true
499 502
500 503 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
501 504
502 505 @parsed_headings = []
503 506 @current_section = 0 if options[:edit_section_links]
504 507 text = parse_non_pre_blocks(text) do |text|
505 508 [:parse_sections, :parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_macros, :parse_headings].each do |method_name|
506 509 send method_name, text, project, obj, attr, only_path, options
507 510 end
508 511 end
509 512
510 513 if @parsed_headings.any?
511 514 replace_toc(text, @parsed_headings)
512 515 end
513 516
514 517 text.html_safe
515 518 end
516 519
517 520 def parse_non_pre_blocks(text)
518 521 s = StringScanner.new(text)
519 522 tags = []
520 523 parsed = ''
521 524 while !s.eos?
522 525 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
523 526 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
524 527 if tags.empty?
525 528 yield text
526 529 end
527 530 parsed << text
528 531 if tag
529 532 if closing
530 533 if tags.last == tag.downcase
531 534 tags.pop
532 535 end
533 536 else
534 537 tags << tag.downcase
535 538 end
536 539 parsed << full_tag
537 540 end
538 541 end
539 542 # Close any non closing tags
540 543 while tag = tags.pop
541 544 parsed << "</#{tag}>"
542 545 end
543 546 parsed.html_safe
544 547 end
545 548
546 549 def parse_inline_attachments(text, project, obj, attr, only_path, options)
547 550 # when using an image link, try to use an attachment, if possible
548 551 if options[:attachments] || (obj && obj.respond_to?(:attachments))
549 552 attachments = options[:attachments] || obj.attachments
550 553 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
551 554 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
552 555 # search for the picture in attachments
553 556 if found = Attachment.latest_attach(attachments, filename)
554 557 image_url = url_for :only_path => only_path, :controller => 'attachments',
555 558 :action => 'download', :id => found
556 559 desc = found.description.to_s.gsub('"', '')
557 560 if !desc.blank? && alttext.blank?
558 561 alt = " title=\"#{desc}\" alt=\"#{desc}\""
559 562 end
560 563 "src=\"#{image_url}\"#{alt}".html_safe
561 564 else
562 565 m.html_safe
563 566 end
564 567 end
565 568 end
566 569 end
567 570
568 571 # Wiki links
569 572 #
570 573 # Examples:
571 574 # [[mypage]]
572 575 # [[mypage|mytext]]
573 576 # wiki links can refer other project wikis, using project name or identifier:
574 577 # [[project:]] -> wiki starting page
575 578 # [[project:|mytext]]
576 579 # [[project:mypage]]
577 580 # [[project:mypage|mytext]]
578 581 def parse_wiki_links(text, project, obj, attr, only_path, options)
579 582 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
580 583 link_project = project
581 584 esc, all, page, title = $1, $2, $3, $5
582 585 if esc.nil?
583 586 if page =~ /^([^\:]+)\:(.*)$/
584 587 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
585 588 page = $2
586 589 title ||= $1 if page.blank?
587 590 end
588 591
589 592 if link_project && link_project.wiki
590 593 # extract anchor
591 594 anchor = nil
592 595 if page =~ /^(.+?)\#(.+)$/
593 596 page, anchor = $1, $2
594 597 end
595 598 anchor = sanitize_anchor_name(anchor) if anchor.present?
596 599 # check if page exists
597 600 wiki_page = link_project.wiki.find_page(page)
598 601 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
599 602 "##{anchor}"
600 603 else
601 604 case options[:wiki_links]
602 605 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
603 606 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
604 607 else
605 608 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
606 609 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
607 610 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
608 611 :id => wiki_page_id, :anchor => anchor, :parent => parent)
609 612 end
610 613 end
611 614 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
612 615 else
613 616 # project or wiki doesn't exist
614 617 all.html_safe
615 618 end
616 619 else
617 620 all.html_safe
618 621 end
619 622 end
620 623 end
621 624
622 625 # Redmine links
623 626 #
624 627 # Examples:
625 628 # Issues:
626 629 # #52 -> Link to issue #52
627 630 # Changesets:
628 631 # r52 -> Link to revision 52
629 632 # commit:a85130f -> Link to scmid starting with a85130f
630 633 # Documents:
631 634 # document#17 -> Link to document with id 17
632 635 # document:Greetings -> Link to the document with title "Greetings"
633 636 # document:"Some document" -> Link to the document with title "Some document"
634 637 # Versions:
635 638 # version#3 -> Link to version with id 3
636 639 # version:1.0.0 -> Link to version named "1.0.0"
637 640 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
638 641 # Attachments:
639 642 # attachment:file.zip -> Link to the attachment of the current object named file.zip
640 643 # Source files:
641 644 # source:some/file -> Link to the file located at /some/file in the project's repository
642 645 # source:some/file@52 -> Link to the file's revision 52
643 646 # source:some/file#L120 -> Link to line 120 of the file
644 647 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
645 648 # export:some/file -> Force the download of the file
646 649 # Forum messages:
647 650 # message#1218 -> Link to message with id 1218
648 651 #
649 652 # Links can refer other objects from other projects, using project identifier:
650 653 # identifier:r52
651 654 # identifier:document:"Some document"
652 655 # identifier:version:1.0.0
653 656 # identifier:source:some/file
654 657 def parse_redmine_links(text, project, obj, attr, only_path, options)
655 658 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-]+):)?(attachment|document|version|forum|news|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m|
656 659 leading, esc, project_prefix, project_identifier, prefix, sep, identifier = $1, $2, $3, $4, $5, $7 || $9, $8 || $10
657 660 link = nil
658 661 if project_identifier
659 662 project = Project.visible.find_by_identifier(project_identifier)
660 663 end
661 664 if esc.nil?
662 665 if prefix.nil? && sep == 'r'
663 666 # project.changesets.visible raises an SQL error because of a double join on repositories
664 667 if project && project.repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(project.repository.id, identifier))
665 668 link = link_to(h("#{project_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
666 669 :class => 'changeset',
667 670 :title => truncate_single_line(changeset.comments, :length => 100))
668 671 end
669 672 elsif sep == '#'
670 673 oid = identifier.to_i
671 674 case prefix
672 675 when nil
673 676 if issue = Issue.visible.find_by_id(oid, :include => :status)
674 677 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
675 678 :class => issue.css_classes,
676 679 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
677 680 end
678 681 when 'document'
679 682 if document = Document.visible.find_by_id(oid)
680 683 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
681 684 :class => 'document'
682 685 end
683 686 when 'version'
684 687 if version = Version.visible.find_by_id(oid)
685 688 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
686 689 :class => 'version'
687 690 end
688 691 when 'message'
689 692 if message = Message.visible.find_by_id(oid, :include => :parent)
690 693 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
691 694 end
692 695 when 'forum'
693 696 if board = Board.visible.find_by_id(oid)
694 697 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
695 698 :class => 'board'
696 699 end
697 700 when 'news'
698 701 if news = News.visible.find_by_id(oid)
699 702 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
700 703 :class => 'news'
701 704 end
702 705 when 'project'
703 706 if p = Project.visible.find_by_id(oid)
704 707 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
705 708 end
706 709 end
707 710 elsif sep == ':'
708 711 # removes the double quotes if any
709 712 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
710 713 case prefix
711 714 when 'document'
712 715 if project && document = project.documents.visible.find_by_title(name)
713 716 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
714 717 :class => 'document'
715 718 end
716 719 when 'version'
717 720 if project && version = project.versions.visible.find_by_name(name)
718 721 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
719 722 :class => 'version'
720 723 end
721 724 when 'forum'
722 725 if project && board = project.boards.visible.find_by_name(name)
723 726 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
724 727 :class => 'board'
725 728 end
726 729 when 'news'
727 730 if project && news = project.news.visible.find_by_title(name)
728 731 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
729 732 :class => 'news'
730 733 end
731 734 when 'commit'
732 735 if project && project.repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", project.repository.id, "#{name}%"]))
733 736 link = link_to h("#{project_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.identifier},
734 737 :class => 'changeset',
735 738 :title => truncate_single_line(h(changeset.comments), :length => 100)
736 739 end
737 740 when 'source', 'export'
738 741 if project && project.repository && User.current.allowed_to?(:browse_repository, project)
739 742 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
740 743 path, rev, anchor = $1, $3, $5
741 744 link = link_to h("#{project_prefix}#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
742 745 :path => to_path_param(path),
743 746 :rev => rev,
744 747 :anchor => anchor,
745 748 :format => (prefix == 'export' ? 'raw' : nil)},
746 749 :class => (prefix == 'export' ? 'source download' : 'source')
747 750 end
748 751 when 'attachment'
749 752 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
750 753 if attachments && attachment = attachments.detect {|a| a.filename == name }
751 754 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
752 755 :class => 'attachment'
753 756 end
754 757 when 'project'
755 758 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
756 759 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
757 760 end
758 761 end
759 762 end
760 763 end
761 764 (leading + (link || "#{project_prefix}#{prefix}#{sep}#{identifier}")).html_safe
762 765 end
763 766 end
764 767
765 768 HEADING_RE = /(<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>)/i unless const_defined?(:HEADING_RE)
766 769
767 770 def parse_sections(text, project, obj, attr, only_path, options)
768 771 return unless options[:edit_section_links]
769 772 text.gsub!(HEADING_RE) do
770 773 @current_section += 1
771 774 if @current_section > 1
772 775 content_tag('div',
773 776 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
774 777 :class => 'contextual',
775 778 :title => l(:button_edit_section)) + $1
776 779 else
777 780 $1
778 781 end
779 782 end
780 783 end
781 784
782 785 # Headings and TOC
783 786 # Adds ids and links to headings unless options[:headings] is set to false
784 787 def parse_headings(text, project, obj, attr, only_path, options)
785 788 return if options[:headings] == false
786 789
787 790 text.gsub!(HEADING_RE) do
788 791 level, attrs, content = $2.to_i, $3, $4
789 792 item = strip_tags(content).strip
790 793 anchor = sanitize_anchor_name(item)
791 794 # used for single-file wiki export
792 795 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
793 796 @parsed_headings << [level, anchor, item]
794 797 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
795 798 end
796 799 end
797 800
798 801 MACROS_RE = /
799 802 (!)? # escaping
800 803 (
801 804 \{\{ # opening tag
802 805 ([\w]+) # macro name
803 806 (\(([^\}]*)\))? # optional arguments
804 807 \}\} # closing tag
805 808 )
806 809 /x unless const_defined?(:MACROS_RE)
807 810
808 811 # Macros substitution
809 812 def parse_macros(text, project, obj, attr, only_path, options)
810 813 text.gsub!(MACROS_RE) do
811 814 esc, all, macro = $1, $2, $3.downcase
812 815 args = ($5 || '').split(',').each(&:strip)
813 816 if esc.nil?
814 817 begin
815 818 exec_macro(macro, obj, args)
816 819 rescue => e
817 820 "<div class=\"flash error\">Error executing the <strong>#{macro}</strong> macro (#{e})</div>"
818 821 end || all
819 822 else
820 823 all
821 824 end
822 825 end
823 826 end
824 827
825 828 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
826 829
827 830 # Renders the TOC with given headings
828 831 def replace_toc(text, headings)
829 832 text.gsub!(TOC_RE) do
830 833 if headings.empty?
831 834 ''
832 835 else
833 836 div_class = 'toc'
834 837 div_class << ' right' if $1 == '>'
835 838 div_class << ' left' if $1 == '<'
836 839 out = "<ul class=\"#{div_class}\"><li>"
837 840 root = headings.map(&:first).min
838 841 current = root
839 842 started = false
840 843 headings.each do |level, anchor, item|
841 844 if level > current
842 845 out << '<ul><li>' * (level - current)
843 846 elsif level < current
844 847 out << "</li></ul>\n" * (current - level) + "</li><li>"
845 848 elsif started
846 849 out << '</li><li>'
847 850 end
848 851 out << "<a href=\"##{anchor}\">#{item}</a>"
849 852 current = level
850 853 started = true
851 854 end
852 855 out << '</li></ul>' * (current - root)
853 856 out << '</li></ul>'
854 857 end
855 858 end
856 859 end
857 860
858 861 # Same as Rails' simple_format helper without using paragraphs
859 862 def simple_format_without_paragraph(text)
860 863 text.to_s.
861 864 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
862 865 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
863 866 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
864 867 html_safe
865 868 end
866 869
867 870 def lang_options_for_select(blank=true)
868 871 (blank ? [["(auto)", ""]] : []) +
869 872 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
870 873 end
871 874
872 875 def label_tag_for(name, option_tags = nil, options = {})
873 876 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
874 877 content_tag("label", label_text)
875 878 end
876 879
877 880 def labelled_tabular_form_for(*args, &proc)
878 881 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_tabular_form_for is deprecated and will be removed in Redmine 1.5. Use #labelled_form_for instead."
879 882 args << {} unless args.last.is_a?(Hash)
880 883 options = args.last
881 884 options[:html] ||= {}
882 885 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
883 886 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
884 887 form_for(*args, &proc)
885 888 end
886 889
887 890 def labelled_form_for(*args, &proc)
888 891 args << {} unless args.last.is_a?(Hash)
889 892 options = args.last
890 893 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
891 894 form_for(*args, &proc)
892 895 end
893 896
894 897 def labelled_fields_for(*args, &proc)
895 898 args << {} unless args.last.is_a?(Hash)
896 899 options = args.last
897 900 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
898 901 fields_for(*args, &proc)
899 902 end
900 903
901 904 def labelled_remote_form_for(*args, &proc)
902 905 args << {} unless args.last.is_a?(Hash)
903 906 options = args.last
904 907 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
905 908 remote_form_for(*args, &proc)
906 909 end
907 910
908 911 def back_url_hidden_field_tag
909 912 back_url = params[:back_url] || request.env['HTTP_REFERER']
910 913 back_url = CGI.unescape(back_url.to_s)
911 914 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
912 915 end
913 916
914 917 def check_all_links(form_name)
915 918 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
916 919 " | ".html_safe +
917 920 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
918 921 end
919 922
920 923 def progress_bar(pcts, options={})
921 924 pcts = [pcts, pcts] unless pcts.is_a?(Array)
922 925 pcts = pcts.collect(&:round)
923 926 pcts[1] = pcts[1] - pcts[0]
924 927 pcts << (100 - pcts[1] - pcts[0])
925 928 width = options[:width] || '100px;'
926 929 legend = options[:legend] || ''
927 930 content_tag('table',
928 931 content_tag('tr',
929 932 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
930 933 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
931 934 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
932 935 ), :class => 'progress', :style => "width: #{width};").html_safe +
933 936 content_tag('p', legend, :class => 'pourcent').html_safe
934 937 end
935 938
936 939 def checked_image(checked=true)
937 940 if checked
938 941 image_tag 'toggle_check.png'
939 942 end
940 943 end
941 944
942 945 def context_menu(url)
943 946 unless @context_menu_included
944 947 content_for :header_tags do
945 948 javascript_include_tag('context_menu') +
946 949 stylesheet_link_tag('context_menu')
947 950 end
948 951 if l(:direction) == 'rtl'
949 952 content_for :header_tags do
950 953 stylesheet_link_tag('context_menu_rtl')
951 954 end
952 955 end
953 956 @context_menu_included = true
954 957 end
955 958 javascript_tag "new ContextMenu('#{ url_for(url) }')"
956 959 end
957 960
958 961 def context_menu_link(name, url, options={})
959 962 options[:class] ||= ''
960 963 if options.delete(:selected)
961 964 options[:class] << ' icon-checked disabled'
962 965 options[:disabled] = true
963 966 end
964 967 if options.delete(:disabled)
965 968 options.delete(:method)
966 969 options.delete(:confirm)
967 970 options.delete(:onclick)
968 971 options[:class] << ' disabled'
969 972 url = '#'
970 973 end
971 974 link_to h(name), url, options
972 975 end
973 976
974 977 def calendar_for(field_id)
975 978 include_calendar_headers_tags
976 979 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
977 980 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
978 981 end
979 982
980 983 def include_calendar_headers_tags
981 984 unless @calendar_headers_tags_included
982 985 @calendar_headers_tags_included = true
983 986 content_for :header_tags do
984 987 start_of_week = case Setting.start_of_week.to_i
985 988 when 1
986 989 'Calendar._FD = 1;' # Monday
987 990 when 7
988 991 'Calendar._FD = 0;' # Sunday
989 992 when 6
990 993 'Calendar._FD = 6;' # Saturday
991 994 else
992 995 '' # use language
993 996 end
994 997
995 998 javascript_include_tag('calendar/calendar') +
996 999 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
997 1000 javascript_tag(start_of_week) +
998 1001 javascript_include_tag('calendar/calendar-setup') +
999 1002 stylesheet_link_tag('calendar')
1000 1003 end
1001 1004 end
1002 1005 end
1003 1006
1004 1007 def content_for(name, content = nil, &block)
1005 1008 @has_content ||= {}
1006 1009 @has_content[name] = true
1007 1010 super(name, content, &block)
1008 1011 end
1009 1012
1010 1013 def has_content?(name)
1011 1014 (@has_content && @has_content[name]) || false
1012 1015 end
1013 1016
1014 1017 def email_delivery_enabled?
1015 1018 !!ActionMailer::Base.perform_deliveries
1016 1019 end
1017 1020
1018 1021 # Returns the avatar image tag for the given +user+ if avatars are enabled
1019 1022 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1020 1023 def avatar(user, options = { })
1021 1024 if Setting.gravatar_enabled?
1022 1025 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
1023 1026 email = nil
1024 1027 if user.respond_to?(:mail)
1025 1028 email = user.mail
1026 1029 elsif user.to_s =~ %r{<(.+?)>}
1027 1030 email = $1
1028 1031 end
1029 1032 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1030 1033 else
1031 1034 ''
1032 1035 end
1033 1036 end
1034 1037
1035 1038 def sanitize_anchor_name(anchor)
1036 1039 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1037 1040 end
1038 1041
1039 1042 # Returns the javascript tags that are included in the html layout head
1040 1043 def javascript_heads
1041 1044 tags = javascript_include_tag(:defaults)
1042 1045 unless User.current.pref.warn_on_leaving_unsaved == '0'
1043 1046 tags << "\n".html_safe + javascript_tag("Event.observe(window, 'load', function(){ new WarnLeavingUnsaved('#{escape_javascript( l(:text_warn_on_leaving_unsaved) )}'); });")
1044 1047 end
1045 1048 tags
1046 1049 end
1047 1050
1048 1051 def favicon
1049 1052 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1050 1053 end
1051 1054
1052 1055 def robot_exclusion_tag
1053 1056 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1054 1057 end
1055 1058
1056 1059 # Returns true if arg is expected in the API response
1057 1060 def include_in_api_response?(arg)
1058 1061 unless @included_in_api_response
1059 1062 param = params[:include]
1060 1063 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1061 1064 @included_in_api_response.collect!(&:strip)
1062 1065 end
1063 1066 @included_in_api_response.include?(arg.to_s)
1064 1067 end
1065 1068
1066 1069 # Returns options or nil if nometa param or X-Redmine-Nometa header
1067 1070 # was set in the request
1068 1071 def api_meta(options)
1069 1072 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1070 1073 # compatibility mode for activeresource clients that raise
1071 1074 # an error when unserializing an array with attributes
1072 1075 nil
1073 1076 else
1074 1077 options
1075 1078 end
1076 1079 end
1077 1080
1078 1081 private
1079 1082
1080 1083 def wiki_helper
1081 1084 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1082 1085 extend helper
1083 1086 return self
1084 1087 end
1085 1088
1086 1089 def link_to_content_update(text, url_params = {}, html_options = {})
1087 1090 link_to(text, url_params, html_options)
1088 1091 end
1089 1092 end
@@ -1,878 +1,887
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../../test_helper', __FILE__)
19 19
20 20 class ApplicationHelperTest < ActionView::TestCase
21 21 fixtures :projects, :roles, :enabled_modules, :users,
22 22 :repositories, :changesets,
23 23 :trackers, :issue_statuses, :issues, :versions, :documents,
24 24 :wikis, :wiki_pages, :wiki_contents,
25 25 :boards, :messages, :news,
26 26 :attachments, :enumerations
27 27
28 28 def setup
29 29 super
30 30 set_tmp_attachments_directory
31 31 end
32 32
33 33 context "#link_to_if_authorized" do
34 34 context "authorized user" do
35 35 should "be tested"
36 36 end
37 37
38 38 context "unauthorized user" do
39 39 should "be tested"
40 40 end
41 41
42 42 should "allow using the :controller and :action for the target link" do
43 43 User.current = User.find_by_login('admin')
44 44
45 45 @project = Issue.first.project # Used by helper
46 46 response = link_to_if_authorized("By controller/action",
47 47 {:controller => 'issues', :action => 'edit', :id => Issue.first.id})
48 48 assert_match /href/, response
49 49 end
50 50
51 51 end
52 52
53 53 def test_auto_links
54 54 to_test = {
55 55 'http://foo.bar' => '<a class="external" href="http://foo.bar">http://foo.bar</a>',
56 56 'http://foo.bar/~user' => '<a class="external" href="http://foo.bar/~user">http://foo.bar/~user</a>',
57 57 'http://foo.bar.' => '<a class="external" href="http://foo.bar">http://foo.bar</a>.',
58 58 'https://foo.bar.' => '<a class="external" href="https://foo.bar">https://foo.bar</a>.',
59 59 'This is a link: http://foo.bar.' => 'This is a link: <a class="external" href="http://foo.bar">http://foo.bar</a>.',
60 60 'A link (eg. http://foo.bar).' => 'A link (eg. <a class="external" href="http://foo.bar">http://foo.bar</a>).',
61 61 '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>.',
62 62 'http://www.foo.bar/Test_(foobar)' => '<a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>',
63 63 '(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>)',
64 64 '(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>)',
65 65 '(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>).',
66 66 '(see "inline link":http://www.foo.bar/Test_(foobar))' => '(see <a href="http://www.foo.bar/Test_(foobar)" class="external">inline link</a>)',
67 67 '(see "inline link":http://www.foo.bar/Test)' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>)',
68 68 '(see "inline link":http://www.foo.bar/Test).' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>).',
69 69 'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>',
70 70 '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>',
71 71 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>',
72 72 'http://foo@www.bar.com' => '<a class="external" href="http://foo@www.bar.com">http://foo@www.bar.com</a>',
73 73 'http://foo:bar@www.bar.com' => '<a class="external" href="http://foo:bar@www.bar.com">http://foo:bar@www.bar.com</a>',
74 74 'ftp://foo.bar' => '<a class="external" href="ftp://foo.bar">ftp://foo.bar</a>',
75 75 'ftps://foo.bar' => '<a class="external" href="ftps://foo.bar">ftps://foo.bar</a>',
76 76 'sftp://foo.bar' => '<a class="external" href="sftp://foo.bar">sftp://foo.bar</a>',
77 77 # two exclamation marks
78 78 '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>',
79 79 # escaping
80 80 'http://foo"bar' => '<a class="external" href="http://foo&quot;bar">http://foo"bar</a>',
81 81 # wrap in angle brackets
82 82 '<http://foo.bar>' => '&lt;<a class="external" href="http://foo.bar">http://foo.bar</a>&gt;'
83 83 }
84 84 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
85 85 end
86 86
87 87 def test_auto_mailto
88 88 assert_equal '<p><a class="email" href="mailto:test@foo.bar">test@foo.bar</a></p>',
89 89 textilizable('test@foo.bar')
90 90 end
91 91
92 92 def test_inline_images
93 93 to_test = {
94 94 '!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
95 95 'floating !>http://foo.bar/image.jpg!' => 'floating <div style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></div>',
96 96 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class <img src="http://foo.bar/image.jpg" class="some-class" alt="" />',
97 97 # inline styles should be stripped
98 98 'with style !{width:100px;height100px}http://foo.bar/image.jpg!' => 'with style <img src="http://foo.bar/image.jpg" alt="" />',
99 99 '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" />',
100 100 '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;" />',
101 101 }
102 102 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
103 103 end
104 104
105 105 def test_inline_images_inside_tags
106 106 raw = <<-RAW
107 107 h1. !foo.png! Heading
108 108
109 109 Centered image:
110 110
111 111 p=. !bar.gif!
112 112 RAW
113 113
114 114 assert textilizable(raw).include?('<img src="foo.png" alt="" />')
115 115 assert textilizable(raw).include?('<img src="bar.gif" alt="" />')
116 116 end
117 117
118 118 def test_attached_images
119 119 to_test = {
120 120 'Inline image: !logo.gif!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
121 121 'Inline image: !logo.GIF!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
122 122 'No match: !ogo.gif!' => 'No match: <img src="ogo.gif" alt="" />',
123 123 'No match: !ogo.GIF!' => 'No match: <img src="ogo.GIF" alt="" />',
124 124 # link image
125 125 '!logo.gif!:http://foo.bar/' => '<a href="http://foo.bar/"><img src="/attachments/download/3" title="This is a logo" alt="This is a logo" /></a>',
126 126 }
127 127 attachments = Attachment.find(:all)
128 128 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
129 129 end
130 130
131 131 def test_attached_images_filename_extension
132 132 set_tmp_attachments_directory
133 133 a1 = Attachment.new(
134 134 :container => Issue.find(1),
135 135 :file => mock_file_with_options({:original_filename => "testtest.JPG"}),
136 136 :author => User.find(1))
137 137 assert a1.save
138 138 assert_equal "testtest.JPG", a1.filename
139 139 assert_equal "image/jpeg", a1.content_type
140 140 assert a1.image?
141 141
142 142 a2 = Attachment.new(
143 143 :container => Issue.find(1),
144 144 :file => mock_file_with_options({:original_filename => "testtest.jpeg"}),
145 145 :author => User.find(1))
146 146 assert a2.save
147 147 assert_equal "testtest.jpeg", a2.filename
148 148 assert_equal "image/jpeg", a2.content_type
149 149 assert a2.image?
150 150
151 151 a3 = Attachment.new(
152 152 :container => Issue.find(1),
153 153 :file => mock_file_with_options({:original_filename => "testtest.JPE"}),
154 154 :author => User.find(1))
155 155 assert a3.save
156 156 assert_equal "testtest.JPE", a3.filename
157 157 assert_equal "image/jpeg", a3.content_type
158 158 assert a3.image?
159 159
160 160 a4 = Attachment.new(
161 161 :container => Issue.find(1),
162 162 :file => mock_file_with_options({:original_filename => "Testtest.BMP"}),
163 163 :author => User.find(1))
164 164 assert a4.save
165 165 assert_equal "Testtest.BMP", a4.filename
166 166 assert_equal "image/x-ms-bmp", a4.content_type
167 167 assert a4.image?
168 168
169 169 to_test = {
170 170 'Inline image: !testtest.jpg!' =>
171 171 'Inline image: <img src="/attachments/download/' + a1.id.to_s + '" alt="" />',
172 172 'Inline image: !testtest.jpeg!' =>
173 173 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '" alt="" />',
174 174 'Inline image: !testtest.jpe!' =>
175 175 'Inline image: <img src="/attachments/download/' + a3.id.to_s + '" alt="" />',
176 176 'Inline image: !testtest.bmp!' =>
177 177 'Inline image: <img src="/attachments/download/' + a4.id.to_s + '" alt="" />',
178 178 }
179 179
180 180 attachments = [a1, a2, a3, a4]
181 181 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
182 182 end
183 183
184 184 def test_attached_images_should_read_later
185 185 set_fixtures_attachments_directory
186 186 a1 = Attachment.find(16)
187 187 assert_equal "testfile.png", a1.filename
188 188 assert a1.readable?
189 189 assert (! a1.visible?(User.anonymous))
190 190 assert a1.visible?(User.find(2))
191 191 a2 = Attachment.find(17)
192 192 assert_equal "testfile.PNG", a2.filename
193 193 assert a2.readable?
194 194 assert (! a2.visible?(User.anonymous))
195 195 assert a2.visible?(User.find(2))
196 196 assert a1.created_on < a2.created_on
197 197
198 198 to_test = {
199 199 'Inline image: !testfile.png!' =>
200 200 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '" alt="" />',
201 201 'Inline image: !Testfile.PNG!' =>
202 202 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '" alt="" />',
203 203 }
204 204 attachments = [a1, a2]
205 205 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
206 206 set_tmp_attachments_directory
207 207 end
208 208
209 209 def test_textile_external_links
210 210 to_test = {
211 211 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>',
212 212 'This is an intern "link":/foo/bar' => 'This is an intern <a href="/foo/bar">link</a>',
213 213 '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>',
214 214 '"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>',
215 215 "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":</p>\n\n\n\t<p>Another paragraph",
216 216 # no multiline link text
217 217 "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",
218 218 # mailto link
219 219 "\"system administrator\":mailto:sysadmin@example.com?subject=redmine%20permissions" => "<a href=\"mailto:sysadmin@example.com?subject=redmine%20permissions\">system administrator</a>",
220 220 # two exclamation marks
221 221 '"a link":http://example.net/path!602815048C7B5C20!302.html' => '<a href="http://example.net/path!602815048C7B5C20!302.html" class="external">a link</a>',
222 222 # escaping
223 223 '"test":http://foo"bar' => '<a href="http://foo&quot;bar" class="external">test</a>',
224 224 }
225 225 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
226 226 end
227 227
228 228 def test_redmine_links
229 229 issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3},
230 230 :class => 'issue status-1 priority-1 overdue', :title => 'Error 281 when updating a recipe (New)')
231 231
232 232 changeset_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
233 233 :class => 'changeset', :title => 'My very first commit')
234 234 changeset_link2 = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
235 235 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
236 236
237 237 document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
238 238 :class => 'document')
239 239
240 240 version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
241 241 :class => 'version')
242 242
243 243 board_url = {:controller => 'boards', :action => 'show', :id => 2, :project_id => 'ecookbook'}
244 244
245 245 message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4}
246 246
247 247 news_url = {:controller => 'news', :action => 'show', :id => 1}
248 248
249 249 project_url = {:controller => 'projects', :action => 'show', :id => 'subproject1'}
250 250
251 251 source_url = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}
252 252 source_url_with_ext = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file.ext']}
253 253
254 254 to_test = {
255 255 # tickets
256 256 '#3, [#3], (#3) and #3.' => "#{issue_link}, [#{issue_link}], (#{issue_link}) and #{issue_link}.",
257 257 # changesets
258 258 'r1' => changeset_link,
259 259 'r1.' => "#{changeset_link}.",
260 260 'r1, r2' => "#{changeset_link}, #{changeset_link2}",
261 261 'r1,r2' => "#{changeset_link},#{changeset_link2}",
262 262 # documents
263 263 'document#1' => document_link,
264 264 'document:"Test document"' => document_link,
265 265 # versions
266 266 'version#2' => version_link,
267 267 'version:1.0' => version_link,
268 268 'version:"1.0"' => version_link,
269 269 # source
270 270 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
271 271 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".",
272 272 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
273 273 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".",
274 274 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
275 275 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",",
276 276 'source:/some/file@52' => link_to('source:/some/file@52', source_url.merge(:rev => 52), :class => 'source'),
277 277 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_ext.merge(:rev => 52), :class => 'source'),
278 278 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url.merge(:anchor => 'L110'), :class => 'source'),
279 279 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext.merge(:anchor => 'L110'), :class => 'source'),
280 280 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url.merge(:rev => 52, :anchor => 'L110'), :class => 'source'),
281 281 'export:/some/file' => link_to('export:/some/file', source_url.merge(:format => 'raw'), :class => 'source download'),
282 282 # forum
283 283 'forum#2' => link_to('Discussion', board_url, :class => 'board'),
284 284 'forum:Discussion' => link_to('Discussion', board_url, :class => 'board'),
285 285 # message
286 286 'message#4' => link_to('Post 2', message_url, :class => 'message'),
287 287 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5', :r => 5), :class => 'message'),
288 288 # news
289 289 'news#1' => link_to('eCookbook first release !', news_url, :class => 'news'),
290 290 'news:"eCookbook first release !"' => link_to('eCookbook first release !', news_url, :class => 'news'),
291 291 # project
292 292 'project#3' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
293 293 'project:subproject1' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
294 294 'project:"eCookbook subProject 1"' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
295 295 # escaping
296 296 '!#3.' => '#3.',
297 297 '!r1' => 'r1',
298 298 '!document#1' => 'document#1',
299 299 '!document:"Test document"' => 'document:"Test document"',
300 300 '!version#2' => 'version#2',
301 301 '!version:1.0' => 'version:1.0',
302 302 '!version:"1.0"' => 'version:"1.0"',
303 303 '!source:/some/file' => 'source:/some/file',
304 304 # not found
305 305 '#0123456789' => '#0123456789',
306 306 # invalid expressions
307 307 'source:' => 'source:',
308 308 # url hash
309 309 "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
310 310 }
311 311 @project = Project.find(1)
312 312 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
313 313 end
314 314
315 315 def test_cross_project_redmine_links
316 316 source_link = link_to('ecookbook:source:/some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']},
317 317 :class => 'source')
318 318
319 319 changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
320 320 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
321 321
322 322 to_test = {
323 323 # documents
324 324 'document:"Test document"' => 'document:"Test document"',
325 325 'ecookbook:document:"Test document"' => '<a href="/documents/1" class="document">Test document</a>',
326 326 'invalid:document:"Test document"' => 'invalid:document:"Test document"',
327 327 # versions
328 328 'version:"1.0"' => 'version:"1.0"',
329 329 'ecookbook:version:"1.0"' => '<a href="/versions/2" class="version">1.0</a>',
330 330 'invalid:version:"1.0"' => 'invalid:version:"1.0"',
331 331 # changeset
332 332 'r2' => 'r2',
333 333 'ecookbook:r2' => changeset_link,
334 334 'invalid:r2' => 'invalid:r2',
335 335 # source
336 336 'source:/some/file' => 'source:/some/file',
337 337 'ecookbook:source:/some/file' => source_link,
338 338 'invalid:source:/some/file' => 'invalid:source:/some/file',
339 339 }
340 340 @project = Project.find(3)
341 341 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
342 342 end
343 343
344 344 def test_redmine_links_git_commit
345 345 changeset_link = link_to('abcd',
346 346 {
347 347 :controller => 'repositories',
348 348 :action => 'revision',
349 349 :id => 'subproject1',
350 350 :rev => 'abcd',
351 351 },
352 352 :class => 'changeset', :title => 'test commit')
353 353 to_test = {
354 354 'commit:abcd' => changeset_link,
355 355 }
356 356 @project = Project.find(3)
357 357 r = Repository::Git.create!(:project => @project, :url => '/tmp/test/git')
358 358 assert r
359 359 c = Changeset.new(:repository => r,
360 360 :committed_on => Time.now,
361 361 :revision => 'abcd',
362 362 :scmid => 'abcd',
363 363 :comments => 'test commit')
364 364 assert( c.save )
365 365 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
366 366 end
367 367
368 368 # TODO: Bazaar commit id contains mail address, so it contains '@' and '_'.
369 369 def test_redmine_links_darcs_commit
370 370 changeset_link = link_to('20080308225258-98289-abcd456efg.gz',
371 371 {
372 372 :controller => 'repositories',
373 373 :action => 'revision',
374 374 :id => 'subproject1',
375 375 :rev => '123',
376 376 },
377 377 :class => 'changeset', :title => 'test commit')
378 378 to_test = {
379 379 'commit:20080308225258-98289-abcd456efg.gz' => changeset_link,
380 380 }
381 381 @project = Project.find(3)
382 382 r = Repository::Darcs.create!(
383 383 :project => @project, :url => '/tmp/test/darcs',
384 384 :log_encoding => 'UTF-8')
385 385 assert r
386 386 c = Changeset.new(:repository => r,
387 387 :committed_on => Time.now,
388 388 :revision => '123',
389 389 :scmid => '20080308225258-98289-abcd456efg.gz',
390 390 :comments => 'test commit')
391 391 assert( c.save )
392 392 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
393 393 end
394 394
395 395 def test_redmine_links_mercurial_commit
396 396 changeset_link_rev = link_to('r123',
397 397 {
398 398 :controller => 'repositories',
399 399 :action => 'revision',
400 400 :id => 'subproject1',
401 401 :rev => '123' ,
402 402 },
403 403 :class => 'changeset', :title => 'test commit')
404 404 changeset_link_commit = link_to('abcd',
405 405 {
406 406 :controller => 'repositories',
407 407 :action => 'revision',
408 408 :id => 'subproject1',
409 409 :rev => 'abcd' ,
410 410 },
411 411 :class => 'changeset', :title => 'test commit')
412 412 to_test = {
413 413 'r123' => changeset_link_rev,
414 414 'commit:abcd' => changeset_link_commit,
415 415 }
416 416 @project = Project.find(3)
417 417 r = Repository::Mercurial.create!(:project => @project, :url => '/tmp/test')
418 418 assert r
419 419 c = Changeset.new(:repository => r,
420 420 :committed_on => Time.now,
421 421 :revision => '123',
422 422 :scmid => 'abcd',
423 423 :comments => 'test commit')
424 424 assert( c.save )
425 425 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
426 426 end
427 427
428 428 def test_attachment_links
429 429 attachment_link = link_to('error281.txt', {:controller => 'attachments', :action => 'download', :id => '1'}, :class => 'attachment')
430 430 to_test = {
431 431 'attachment:error281.txt' => attachment_link
432 432 }
433 433 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => Issue.find(3).attachments), "#{text} failed" }
434 434 end
435 435
436 436 def test_wiki_links
437 437 to_test = {
438 438 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
439 439 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
440 440 # title content should be formatted
441 441 '[[Another page|With _styled_ *title*]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">With <em>styled</em> <strong>title</strong></a>',
442 442 '[[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>',
443 443 # link with anchor
444 444 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
445 445 '[[Another page#anchor|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page#anchor" class="wiki-page">Page</a>',
446 446 # page that doesn't exist
447 447 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
448 448 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">404</a>',
449 449 # link to another project wiki
450 450 '[[onlinestore:]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">onlinestore</a>',
451 451 '[[onlinestore:|Wiki]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">Wiki</a>',
452 452 '[[onlinestore:Start page]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Start page</a>',
453 453 '[[onlinestore:Start page|Text]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Text</a>',
454 454 '[[onlinestore:Unknown page]]' => '<a href="/projects/onlinestore/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
455 455 # striked through link
456 456 '-[[Another page|Page]]-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a></del>',
457 457 '-[[Another page|Page]] link-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a> link</del>',
458 458 # escaping
459 459 '![[Another page|Page]]' => '[[Another page|Page]]',
460 460 # project does not exist
461 461 '[[unknowproject:Start]]' => '[[unknowproject:Start]]',
462 462 '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]',
463 463 }
464 464
465 465 @project = Project.find(1)
466 466 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
467 467 end
468 468
469 469 def test_wiki_links_within_local_file_generation_context
470 470
471 471 to_test = {
472 472 # link to a page
473 473 '[[CookBook documentation]]' => '<a href="CookBook_documentation.html" class="wiki-page">CookBook documentation</a>',
474 474 '[[CookBook documentation|documentation]]' => '<a href="CookBook_documentation.html" class="wiki-page">documentation</a>',
475 475 '[[CookBook documentation#One-section]]' => '<a href="CookBook_documentation.html#One-section" class="wiki-page">CookBook documentation</a>',
476 476 '[[CookBook documentation#One-section|documentation]]' => '<a href="CookBook_documentation.html#One-section" class="wiki-page">documentation</a>',
477 477 # page that doesn't exist
478 478 '[[Unknown page]]' => '<a href="Unknown_page.html" class="wiki-page new">Unknown page</a>',
479 479 '[[Unknown page|404]]' => '<a href="Unknown_page.html" class="wiki-page new">404</a>',
480 480 '[[Unknown page#anchor]]' => '<a href="Unknown_page.html#anchor" class="wiki-page new">Unknown page</a>',
481 481 '[[Unknown page#anchor|404]]' => '<a href="Unknown_page.html#anchor" class="wiki-page new">404</a>',
482 482 }
483 483
484 484 @project = Project.find(1)
485 485
486 486 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :local) }
487 487 end
488 488
489 489 def test_wiki_links_within_wiki_page_context
490 490
491 491 page = WikiPage.find_by_title('Another_page' )
492 492
493 493 to_test = {
494 494 # link to another page
495 495 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
496 496 '[[CookBook documentation|documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">documentation</a>',
497 497 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
498 498 '[[CookBook documentation#One-section|documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">documentation</a>',
499 499 # link to the current page
500 500 '[[Another page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Another page</a>',
501 501 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
502 502 '[[Another page#anchor]]' => '<a href="#anchor" class="wiki-page">Another page</a>',
503 503 '[[Another page#anchor|Page]]' => '<a href="#anchor" class="wiki-page">Page</a>',
504 504 # page that doesn't exist
505 505 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page" class="wiki-page new">Unknown page</a>',
506 506 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page" class="wiki-page new">404</a>',
507 507 '[[Unknown page#anchor]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor" class="wiki-page new">Unknown page</a>',
508 508 '[[Unknown page#anchor|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor" class="wiki-page new">404</a>',
509 509 }
510 510
511 511 @project = Project.find(1)
512 512
513 513 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(WikiContent.new( :text => text, :page => page ), :text) }
514 514 end
515 515
516 516 def test_wiki_links_anchor_option_should_prepend_page_title_to_href
517 517
518 518 to_test = {
519 519 # link to a page
520 520 '[[CookBook documentation]]' => '<a href="#CookBook_documentation" class="wiki-page">CookBook documentation</a>',
521 521 '[[CookBook documentation|documentation]]' => '<a href="#CookBook_documentation" class="wiki-page">documentation</a>',
522 522 '[[CookBook documentation#One-section]]' => '<a href="#CookBook_documentation_One-section" class="wiki-page">CookBook documentation</a>',
523 523 '[[CookBook documentation#One-section|documentation]]' => '<a href="#CookBook_documentation_One-section" class="wiki-page">documentation</a>',
524 524 # page that doesn't exist
525 525 '[[Unknown page]]' => '<a href="#Unknown_page" class="wiki-page new">Unknown page</a>',
526 526 '[[Unknown page|404]]' => '<a href="#Unknown_page" class="wiki-page new">404</a>',
527 527 '[[Unknown page#anchor]]' => '<a href="#Unknown_page_anchor" class="wiki-page new">Unknown page</a>',
528 528 '[[Unknown page#anchor|404]]' => '<a href="#Unknown_page_anchor" class="wiki-page new">404</a>',
529 529 }
530 530
531 531 @project = Project.find(1)
532 532
533 533 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :anchor) }
534 534 end
535 535
536 536 def test_html_tags
537 537 to_test = {
538 538 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
539 539 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
540 540 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
541 541 # do not escape pre/code tags
542 542 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
543 543 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
544 544 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
545 545 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
546 546 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
547 547 # remove attributes except class
548 548 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
549 549 '<pre class="foo">some text</pre>' => '<pre class="foo">some text</pre>',
550 550 "<pre class='foo bar'>some text</pre>" => "<pre class='foo bar'>some text</pre>",
551 551 '<pre class="foo bar">some text</pre>' => '<pre class="foo bar">some text</pre>',
552 552 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
553 553 # xss
554 554 '<pre><code class=""onmouseover="alert(1)">text</code></pre>' => '<pre><code>text</code></pre>',
555 555 '<pre class=""onmouseover="alert(1)">text</pre>' => '<pre>text</pre>',
556 556 }
557 557 to_test.each { |text, result| assert_equal result, textilizable(text) }
558 558 end
559 559
560 560 def test_allowed_html_tags
561 561 to_test = {
562 562 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
563 563 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
564 564 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
565 565 }
566 566 to_test.each { |text, result| assert_equal result, textilizable(text) }
567 567 end
568 568
569 569 def test_pre_tags
570 570 raw = <<-RAW
571 571 Before
572 572
573 573 <pre>
574 574 <prepared-statement-cache-size>32</prepared-statement-cache-size>
575 575 </pre>
576 576
577 577 After
578 578 RAW
579 579
580 580 expected = <<-EXPECTED
581 581 <p>Before</p>
582 582 <pre>
583 583 &lt;prepared-statement-cache-size&gt;32&lt;/prepared-statement-cache-size&gt;
584 584 </pre>
585 585 <p>After</p>
586 586 EXPECTED
587 587
588 588 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
589 589 end
590 590
591 591 def test_pre_content_should_not_parse_wiki_and_redmine_links
592 592 raw = <<-RAW
593 593 [[CookBook documentation]]
594 594
595 595 #1
596 596
597 597 <pre>
598 598 [[CookBook documentation]]
599 599
600 600 #1
601 601 </pre>
602 602 RAW
603 603
604 604 expected = <<-EXPECTED
605 605 <p><a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a></p>
606 606 <p><a href="/issues/1" class="issue status-1 priority-1" title="Can't print recipes (New)">#1</a></p>
607 607 <pre>
608 608 [[CookBook documentation]]
609 609
610 610 #1
611 611 </pre>
612 612 EXPECTED
613 613
614 614 @project = Project.find(1)
615 615 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
616 616 end
617 617
618 618 def test_non_closing_pre_blocks_should_be_closed
619 619 raw = <<-RAW
620 620 <pre><code>
621 621 RAW
622 622
623 623 expected = <<-EXPECTED
624 624 <pre><code>
625 625 </code></pre>
626 626 EXPECTED
627 627
628 628 @project = Project.find(1)
629 629 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
630 630 end
631 631
632 632 def test_syntax_highlight
633 633 raw = <<-RAW
634 634 <pre><code class="ruby">
635 635 # Some ruby code here
636 636 </code></pre>
637 637 RAW
638 638
639 639 expected = <<-EXPECTED
640 640 <pre><code class="ruby syntaxhl"><span class=\"CodeRay\"><span class="line-numbers">1</span><span class="comment"># Some ruby code here</span></span>
641 641 </code></pre>
642 642 EXPECTED
643 643
644 644 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
645 645 end
646 646
647 647 def test_wiki_links_in_tables
648 648 to_test = {"|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" =>
649 649 '<tr><td><a href="/projects/ecookbook/wiki/Page" class="wiki-page new">Link title</a></td>' +
650 650 '<td><a href="/projects/ecookbook/wiki/Other_Page" class="wiki-page new">Other title</a></td>' +
651 651 '</tr><tr><td>Cell 21</td><td><a href="/projects/ecookbook/wiki/Last_page" class="wiki-page new">Last page</a></td></tr>'
652 652 }
653 653 @project = Project.find(1)
654 654 to_test.each { |text, result| assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '') }
655 655 end
656 656
657 657 def test_text_formatting
658 658 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
659 659 '(_text within parentheses_)' => '(<em>text within parentheses</em>)',
660 660 'a *Humane Web* Text Generator' => 'a <strong>Humane Web</strong> Text Generator',
661 661 '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>',
662 662 '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',
663 663 }
664 664 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
665 665 end
666 666
667 667 def test_wiki_horizontal_rule
668 668 assert_equal '<hr />', textilizable('---')
669 669 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
670 670 end
671 671
672 672 def test_footnotes
673 673 raw = <<-RAW
674 674 This is some text[1].
675 675
676 676 fn1. This is the foot note
677 677 RAW
678 678
679 679 expected = <<-EXPECTED
680 680 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
681 681 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
682 682 EXPECTED
683 683
684 684 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
685 685 end
686 686
687 687 def test_headings
688 688 raw = 'h1. Some heading'
689 689 expected = %|<a name="Some-heading"></a>\n<h1 >Some heading<a href="#Some-heading" class="wiki-anchor">&para;</a></h1>|
690 690
691 691 assert_equal expected, textilizable(raw)
692 692 end
693 693
694 694 def test_headings_with_special_chars
695 695 # This test makes sure that the generated anchor names match the expected
696 696 # ones even if the heading text contains unconventional characters
697 697 raw = 'h1. Some heading related to version 0.5'
698 698 anchor = sanitize_anchor_name("Some-heading-related-to-version-0.5")
699 699 expected = %|<a name="#{anchor}"></a>\n<h1 >Some heading related to version 0.5<a href="##{anchor}" class="wiki-anchor">&para;</a></h1>|
700 700
701 701 assert_equal expected, textilizable(raw)
702 702 end
703 703
704 704 def test_headings_in_wiki_single_page_export_should_be_prepended_with_page_title
705 705 page = WikiPage.new( :title => 'Page Title', :wiki_id => 1 )
706 706 content = WikiContent.new( :text => 'h1. Some heading', :page => page )
707 707
708 708 expected = %|<a name="Page_Title_Some-heading"></a>\n<h1 >Some heading<a href="#Page_Title_Some-heading" class="wiki-anchor">&para;</a></h1>|
709 709
710 710 assert_equal expected, textilizable(content, :text, :wiki_links => :anchor )
711 711 end
712 712
713 713 def test_table_of_content
714 714 raw = <<-RAW
715 715 {{toc}}
716 716
717 717 h1. Title
718 718
719 719 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
720 720
721 721 h2. Subtitle with a [[Wiki]] link
722 722
723 723 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
724 724
725 725 h2. Subtitle with [[Wiki|another Wiki]] link
726 726
727 727 h2. Subtitle with %{color:red}red text%
728 728
729 729 <pre>
730 730 some code
731 731 </pre>
732 732
733 733 h3. Subtitle with *some* _modifiers_
734 734
735 735 h1. Another title
736 736
737 737 h3. An "Internet link":http://www.redmine.org/ inside subtitle
738 738
739 739 h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
740 740
741 741 RAW
742 742
743 743 expected = '<ul class="toc">' +
744 744 '<li><a href="#Title">Title</a>' +
745 745 '<ul>' +
746 746 '<li><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
747 747 '<li><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
748 748 '<li><a href="#Subtitle-with-red-text">Subtitle with red text</a>' +
749 749 '<ul>' +
750 750 '<li><a href="#Subtitle-with-some-modifiers">Subtitle with some modifiers</a></li>' +
751 751 '</ul>' +
752 752 '</li>' +
753 753 '</ul>' +
754 754 '</li>' +
755 755 '<li><a href="#Another-title">Another title</a>' +
756 756 '<ul>' +
757 757 '<li>' +
758 758 '<ul>' +
759 759 '<li><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
760 760 '</ul>' +
761 761 '</li>' +
762 762 '<li><a href="#Project-Name">Project Name</a></li>' +
763 763 '</ul>' +
764 764 '</li>' +
765 765 '</ul>'
766 766
767 767 @project = Project.find(1)
768 768 assert textilizable(raw).gsub("\n", "").include?(expected)
769 769 end
770 770
771 771 def test_table_of_content_should_contain_included_page_headings
772 772 raw = <<-RAW
773 773 {{toc}}
774 774
775 775 h1. Included
776 776
777 777 {{include(Child_1)}}
778 778 RAW
779 779
780 780 expected = '<ul class="toc">' +
781 781 '<li><a href="#Included">Included</a></li>' +
782 782 '<li><a href="#Child-page-1">Child page 1</a></li>' +
783 783 '</ul>'
784 784
785 785 @project = Project.find(1)
786 786 assert textilizable(raw).gsub("\n", "").include?(expected)
787 787 end
788 788
789 789 def test_default_formatter
790 790 with_settings :text_formatting => 'unknown' do
791 791 text = 'a *link*: http://www.example.net/'
792 792 assert_equal '<p>a *link*: <a href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
793 793 end
794 794 end
795 795
796 796 def test_due_date_distance_in_words
797 797 to_test = { Date.today => 'Due in 0 days',
798 798 Date.today + 1 => 'Due in 1 day',
799 799 Date.today + 100 => 'Due in about 3 months',
800 800 Date.today + 20000 => 'Due in over 54 years',
801 801 Date.today - 1 => '1 day late',
802 802 Date.today - 100 => 'about 3 months late',
803 803 Date.today - 20000 => 'over 54 years late',
804 804 }
805 805 ::I18n.locale = :en
806 806 to_test.each do |date, expected|
807 807 assert_equal expected, due_date_distance_in_words(date)
808 808 end
809 809 end
810 810
811 811 def test_avatar
812 812 # turn on avatars
813 813 Setting.gravatar_enabled = '1'
814 814 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
815 815 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
816 816 assert_nil avatar('jsmith')
817 817 assert_nil avatar(nil)
818 818
819 819 # turn off avatars
820 820 Setting.gravatar_enabled = '0'
821 821 assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
822 822 end
823 823
824 824 def test_link_to_user
825 825 user = User.find(2)
826 826 t = link_to_user(user)
827 827 assert_equal "<a href=\"/users/2\">#{ user.name }</a>", t
828 828 end
829 829
830 830 def test_link_to_user_should_not_link_to_locked_user
831 831 user = User.find(5)
832 832 assert user.locked?
833 833 t = link_to_user(user)
834 834 assert_equal user.name, t
835 835 end
836 836
837 837 def test_link_to_user_should_not_link_to_anonymous
838 838 user = User.anonymous
839 839 assert user.anonymous?
840 840 t = link_to_user(user)
841 841 assert_equal ::I18n.t(:label_user_anonymous), t
842 842 end
843 843
844 844 def test_link_to_project
845 845 project = Project.find(1)
846 846 assert_equal %(<a href="/projects/ecookbook">eCookbook</a>),
847 847 link_to_project(project)
848 848 assert_equal %(<a href="/projects/ecookbook/settings">eCookbook</a>),
849 849 link_to_project(project, :action => 'settings')
850 850 assert_equal %(<a href="http://test.host/projects/ecookbook?jump=blah">eCookbook</a>),
851 851 link_to_project(project, {:only_path => false, :jump => 'blah'})
852 852 assert_equal %(<a href="/projects/ecookbook/settings" class="project">eCookbook</a>),
853 853 link_to_project(project, {:action => 'settings'}, :class => "project")
854 854 end
855 855
856 856 def test_principals_options_for_select_with_users
857 User.current = nil
857 858 users = [User.find(2), User.find(4)]
858 859 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>),
859 860 principals_options_for_select(users)
860 861 end
861 862
862 863 def test_principals_options_for_select_with_selected
864 User.current = nil
863 865 users = [User.find(2), User.find(4)]
864 866 assert_equal %(<option value="2">John Smith</option><option value="4" selected="selected">Robert Hill</option>),
865 867 principals_options_for_select(users, User.find(4))
866 868 end
867 869
868 870 def test_principals_options_for_select_with_users_and_groups
871 User.current = nil
869 872 users = [User.find(2), Group.find(11), User.find(4), Group.find(10)]
870 873 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>) +
871 874 %(<optgroup label="Groups"><option value="10">A Team</option><option value="11">B Team</option></optgroup>),
872 875 principals_options_for_select(users)
873 876 end
874 877
875 878 def test_principals_options_for_select_with_empty_collection
876 879 assert_equal '', principals_options_for_select([])
877 880 end
881
882 def test_principals_options_for_select_should_include_me_option_when_current_user_is_in_collection
883 users = [User.find(2), User.find(4)]
884 User.current = User.find(4)
885 assert_include '<option value="4"><< me >></option>', principals_options_for_select(users)
886 end
878 887 end
General Comments 0
You need to be logged in to leave comments. Login now