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