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