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