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