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