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