##// END OF EJS Templates
Adds a named route for thumbnails and use route helper in #thumbnail_tag....
Jean-Philippe Lang -
r10958:6ce28450c004
parent child
Show More
@@ -1,1241 +1,1241
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 link_to image_tag(url_for(:controller => 'attachments', :action => 'thumbnail', :id => attachment)),
164 {:controller => 'attachments', :action => 'show', :id => attachment, :filename => attachment.filename},
163 link_to image_tag(thumbnail_path(attachment)),
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 = url_for :only_path => only_path, :controller => 'attachments',
563 563 :action => 'download', :id => found
564 564 desc = found.description.to_s.gsub('"', '')
565 565 if !desc.blank? && alttext.blank?
566 566 alt = " title=\"#{desc}\" alt=\"#{desc}\""
567 567 end
568 568 "src=\"#{image_url}\"#{alt}"
569 569 else
570 570 m
571 571 end
572 572 end
573 573 end
574 574 end
575 575
576 576 # Wiki links
577 577 #
578 578 # Examples:
579 579 # [[mypage]]
580 580 # [[mypage|mytext]]
581 581 # wiki links can refer other project wikis, using project name or identifier:
582 582 # [[project:]] -> wiki starting page
583 583 # [[project:|mytext]]
584 584 # [[project:mypage]]
585 585 # [[project:mypage|mytext]]
586 586 def parse_wiki_links(text, project, obj, attr, only_path, options)
587 587 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
588 588 link_project = project
589 589 esc, all, page, title = $1, $2, $3, $5
590 590 if esc.nil?
591 591 if page =~ /^([^\:]+)\:(.*)$/
592 592 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
593 593 page = $2
594 594 title ||= $1 if page.blank?
595 595 end
596 596
597 597 if link_project && link_project.wiki
598 598 # extract anchor
599 599 anchor = nil
600 600 if page =~ /^(.+?)\#(.+)$/
601 601 page, anchor = $1, $2
602 602 end
603 603 anchor = sanitize_anchor_name(anchor) if anchor.present?
604 604 # check if page exists
605 605 wiki_page = link_project.wiki.find_page(page)
606 606 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
607 607 "##{anchor}"
608 608 else
609 609 case options[:wiki_links]
610 610 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
611 611 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
612 612 else
613 613 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
614 614 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
615 615 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
616 616 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
617 617 end
618 618 end
619 619 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
620 620 else
621 621 # project or wiki doesn't exist
622 622 all
623 623 end
624 624 else
625 625 all
626 626 end
627 627 end
628 628 end
629 629
630 630 # Redmine links
631 631 #
632 632 # Examples:
633 633 # Issues:
634 634 # #52 -> Link to issue #52
635 635 # Changesets:
636 636 # r52 -> Link to revision 52
637 637 # commit:a85130f -> Link to scmid starting with a85130f
638 638 # Documents:
639 639 # document#17 -> Link to document with id 17
640 640 # document:Greetings -> Link to the document with title "Greetings"
641 641 # document:"Some document" -> Link to the document with title "Some document"
642 642 # Versions:
643 643 # version#3 -> Link to version with id 3
644 644 # version:1.0.0 -> Link to version named "1.0.0"
645 645 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
646 646 # Attachments:
647 647 # attachment:file.zip -> Link to the attachment of the current object named file.zip
648 648 # Source files:
649 649 # source:some/file -> Link to the file located at /some/file in the project's repository
650 650 # source:some/file@52 -> Link to the file's revision 52
651 651 # source:some/file#L120 -> Link to line 120 of the file
652 652 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
653 653 # export:some/file -> Force the download of the file
654 654 # Forum messages:
655 655 # message#1218 -> Link to message with id 1218
656 656 #
657 657 # Links can refer other objects from other projects, using project identifier:
658 658 # identifier:r52
659 659 # identifier:document:"Some document"
660 660 # identifier:version:1.0.0
661 661 # identifier:source:some/file
662 662 def parse_redmine_links(text, project, obj, attr, only_path, options)
663 663 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|
664 664 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
665 665 link = nil
666 666 if project_identifier
667 667 project = Project.visible.find_by_identifier(project_identifier)
668 668 end
669 669 if esc.nil?
670 670 if prefix.nil? && sep == 'r'
671 671 if project
672 672 repository = nil
673 673 if repo_identifier
674 674 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
675 675 else
676 676 repository = project.repository
677 677 end
678 678 # project.changesets.visible raises an SQL error because of a double join on repositories
679 679 if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier))
680 680 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.revision},
681 681 :class => 'changeset',
682 682 :title => truncate_single_line(changeset.comments, :length => 100))
683 683 end
684 684 end
685 685 elsif sep == '#'
686 686 oid = identifier.to_i
687 687 case prefix
688 688 when nil
689 689 if oid.to_s == identifier && issue = Issue.visible.find_by_id(oid, :include => :status)
690 690 anchor = comment_id ? "note-#{comment_id}" : nil
691 691 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor},
692 692 :class => issue.css_classes,
693 693 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
694 694 end
695 695 when 'document'
696 696 if document = Document.visible.find_by_id(oid)
697 697 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
698 698 :class => 'document'
699 699 end
700 700 when 'version'
701 701 if version = Version.visible.find_by_id(oid)
702 702 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
703 703 :class => 'version'
704 704 end
705 705 when 'message'
706 706 if message = Message.visible.find_by_id(oid, :include => :parent)
707 707 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
708 708 end
709 709 when 'forum'
710 710 if board = Board.visible.find_by_id(oid)
711 711 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
712 712 :class => 'board'
713 713 end
714 714 when 'news'
715 715 if news = News.visible.find_by_id(oid)
716 716 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
717 717 :class => 'news'
718 718 end
719 719 when 'project'
720 720 if p = Project.visible.find_by_id(oid)
721 721 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
722 722 end
723 723 end
724 724 elsif sep == ':'
725 725 # removes the double quotes if any
726 726 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
727 727 case prefix
728 728 when 'document'
729 729 if project && document = project.documents.visible.find_by_title(name)
730 730 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
731 731 :class => 'document'
732 732 end
733 733 when 'version'
734 734 if project && version = project.versions.visible.find_by_name(name)
735 735 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
736 736 :class => 'version'
737 737 end
738 738 when 'forum'
739 739 if project && board = project.boards.visible.find_by_name(name)
740 740 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
741 741 :class => 'board'
742 742 end
743 743 when 'news'
744 744 if project && news = project.news.visible.find_by_title(name)
745 745 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
746 746 :class => 'news'
747 747 end
748 748 when 'commit', 'source', 'export'
749 749 if project
750 750 repository = nil
751 751 if name =~ %r{^(([a-z0-9\-]+)\|)(.+)$}
752 752 repo_prefix, repo_identifier, name = $1, $2, $3
753 753 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
754 754 else
755 755 repository = project.repository
756 756 end
757 757 if prefix == 'commit'
758 758 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
759 759 link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
760 760 :class => 'changeset',
761 761 :title => truncate_single_line(h(changeset.comments), :length => 100)
762 762 end
763 763 else
764 764 if repository && User.current.allowed_to?(:browse_repository, project)
765 765 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
766 766 path, rev, anchor = $1, $3, $5
767 767 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
768 768 :path => to_path_param(path),
769 769 :rev => rev,
770 770 :anchor => anchor},
771 771 :class => (prefix == 'export' ? 'source download' : 'source')
772 772 end
773 773 end
774 774 repo_prefix = nil
775 775 end
776 776 when 'attachment'
777 777 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
778 778 if attachments && attachment = attachments.detect {|a| a.filename == name }
779 779 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
780 780 :class => 'attachment'
781 781 end
782 782 when 'project'
783 783 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
784 784 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
785 785 end
786 786 end
787 787 end
788 788 end
789 789 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
790 790 end
791 791 end
792 792
793 793 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
794 794
795 795 def parse_sections(text, project, obj, attr, only_path, options)
796 796 return unless options[:edit_section_links]
797 797 text.gsub!(HEADING_RE) do
798 798 heading = $1
799 799 @current_section += 1
800 800 if @current_section > 1
801 801 content_tag('div',
802 802 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
803 803 :class => 'contextual',
804 804 :title => l(:button_edit_section)) + heading.html_safe
805 805 else
806 806 heading
807 807 end
808 808 end
809 809 end
810 810
811 811 # Headings and TOC
812 812 # Adds ids and links to headings unless options[:headings] is set to false
813 813 def parse_headings(text, project, obj, attr, only_path, options)
814 814 return if options[:headings] == false
815 815
816 816 text.gsub!(HEADING_RE) do
817 817 level, attrs, content = $2.to_i, $3, $4
818 818 item = strip_tags(content).strip
819 819 anchor = sanitize_anchor_name(item)
820 820 # used for single-file wiki export
821 821 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
822 822 @heading_anchors[anchor] ||= 0
823 823 idx = (@heading_anchors[anchor] += 1)
824 824 if idx > 1
825 825 anchor = "#{anchor}-#{idx}"
826 826 end
827 827 @parsed_headings << [level, anchor, item]
828 828 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
829 829 end
830 830 end
831 831
832 832 MACROS_RE = /(
833 833 (!)? # escaping
834 834 (
835 835 \{\{ # opening tag
836 836 ([\w]+) # macro name
837 837 (\(([^\n\r]*?)\))? # optional arguments
838 838 ([\n\r].*?[\n\r])? # optional block of text
839 839 \}\} # closing tag
840 840 )
841 841 )/mx unless const_defined?(:MACROS_RE)
842 842
843 843 MACRO_SUB_RE = /(
844 844 \{\{
845 845 macro\((\d+)\)
846 846 \}\}
847 847 )/x unless const_defined?(:MACRO_SUB_RE)
848 848
849 849 # Extracts macros from text
850 850 def catch_macros(text)
851 851 macros = {}
852 852 text.gsub!(MACROS_RE) do
853 853 all, macro = $1, $4.downcase
854 854 if macro_exists?(macro) || all =~ MACRO_SUB_RE
855 855 index = macros.size
856 856 macros[index] = all
857 857 "{{macro(#{index})}}"
858 858 else
859 859 all
860 860 end
861 861 end
862 862 macros
863 863 end
864 864
865 865 # Executes and replaces macros in text
866 866 def inject_macros(text, obj, macros, execute=true)
867 867 text.gsub!(MACRO_SUB_RE) do
868 868 all, index = $1, $2.to_i
869 869 orig = macros.delete(index)
870 870 if execute && orig && orig =~ MACROS_RE
871 871 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
872 872 if esc.nil?
873 873 h(exec_macro(macro, obj, args, block) || all)
874 874 else
875 875 h(all)
876 876 end
877 877 elsif orig
878 878 h(orig)
879 879 else
880 880 h(all)
881 881 end
882 882 end
883 883 end
884 884
885 885 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
886 886
887 887 # Renders the TOC with given headings
888 888 def replace_toc(text, headings)
889 889 text.gsub!(TOC_RE) do
890 890 # Keep only the 4 first levels
891 891 headings = headings.select{|level, anchor, item| level <= 4}
892 892 if headings.empty?
893 893 ''
894 894 else
895 895 div_class = 'toc'
896 896 div_class << ' right' if $1 == '>'
897 897 div_class << ' left' if $1 == '<'
898 898 out = "<ul class=\"#{div_class}\"><li>"
899 899 root = headings.map(&:first).min
900 900 current = root
901 901 started = false
902 902 headings.each do |level, anchor, item|
903 903 if level > current
904 904 out << '<ul><li>' * (level - current)
905 905 elsif level < current
906 906 out << "</li></ul>\n" * (current - level) + "</li><li>"
907 907 elsif started
908 908 out << '</li><li>'
909 909 end
910 910 out << "<a href=\"##{anchor}\">#{item}</a>"
911 911 current = level
912 912 started = true
913 913 end
914 914 out << '</li></ul>' * (current - root)
915 915 out << '</li></ul>'
916 916 end
917 917 end
918 918 end
919 919
920 920 # Same as Rails' simple_format helper without using paragraphs
921 921 def simple_format_without_paragraph(text)
922 922 text.to_s.
923 923 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
924 924 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
925 925 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
926 926 html_safe
927 927 end
928 928
929 929 def lang_options_for_select(blank=true)
930 930 (blank ? [["(auto)", ""]] : []) + languages_options
931 931 end
932 932
933 933 def label_tag_for(name, option_tags = nil, options = {})
934 934 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
935 935 content_tag("label", label_text)
936 936 end
937 937
938 938 def labelled_form_for(*args, &proc)
939 939 args << {} unless args.last.is_a?(Hash)
940 940 options = args.last
941 941 if args.first.is_a?(Symbol)
942 942 options.merge!(:as => args.shift)
943 943 end
944 944 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
945 945 form_for(*args, &proc)
946 946 end
947 947
948 948 def labelled_fields_for(*args, &proc)
949 949 args << {} unless args.last.is_a?(Hash)
950 950 options = args.last
951 951 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
952 952 fields_for(*args, &proc)
953 953 end
954 954
955 955 def labelled_remote_form_for(*args, &proc)
956 956 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_remote_form_for is deprecated and will be removed in Redmine 2.2."
957 957 args << {} unless args.last.is_a?(Hash)
958 958 options = args.last
959 959 options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
960 960 form_for(*args, &proc)
961 961 end
962 962
963 963 def error_messages_for(*objects)
964 964 html = ""
965 965 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
966 966 errors = objects.map {|o| o.errors.full_messages}.flatten
967 967 if errors.any?
968 968 html << "<div id='errorExplanation'><ul>\n"
969 969 errors.each do |error|
970 970 html << "<li>#{h error}</li>\n"
971 971 end
972 972 html << "</ul></div>\n"
973 973 end
974 974 html.html_safe
975 975 end
976 976
977 977 def delete_link(url, options={})
978 978 options = {
979 979 :method => :delete,
980 980 :data => {:confirm => l(:text_are_you_sure)},
981 981 :class => 'icon icon-del'
982 982 }.merge(options)
983 983
984 984 link_to l(:button_delete), url, options
985 985 end
986 986
987 987 def preview_link(url, form, target='preview', options={})
988 988 content_tag 'a', l(:label_preview), {
989 989 :href => "#",
990 990 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
991 991 :accesskey => accesskey(:preview)
992 992 }.merge(options)
993 993 end
994 994
995 995 def link_to_function(name, function, html_options={})
996 996 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
997 997 end
998 998
999 999 # Helper to render JSON in views
1000 1000 def raw_json(arg)
1001 1001 arg.to_json.to_s.gsub('/', '\/').html_safe
1002 1002 end
1003 1003
1004 1004 def back_url
1005 1005 url = params[:back_url]
1006 1006 if url.nil? && referer = request.env['HTTP_REFERER']
1007 1007 url = CGI.unescape(referer.to_s)
1008 1008 end
1009 1009 url
1010 1010 end
1011 1011
1012 1012 def back_url_hidden_field_tag
1013 1013 url = back_url
1014 1014 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1015 1015 end
1016 1016
1017 1017 def check_all_links(form_name)
1018 1018 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1019 1019 " | ".html_safe +
1020 1020 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1021 1021 end
1022 1022
1023 1023 def progress_bar(pcts, options={})
1024 1024 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1025 1025 pcts = pcts.collect(&:round)
1026 1026 pcts[1] = pcts[1] - pcts[0]
1027 1027 pcts << (100 - pcts[1] - pcts[0])
1028 1028 width = options[:width] || '100px;'
1029 1029 legend = options[:legend] || ''
1030 1030 content_tag('table',
1031 1031 content_tag('tr',
1032 1032 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1033 1033 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1034 1034 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1035 1035 ), :class => 'progress', :style => "width: #{width};").html_safe +
1036 1036 content_tag('p', legend, :class => 'percent').html_safe
1037 1037 end
1038 1038
1039 1039 def checked_image(checked=true)
1040 1040 if checked
1041 1041 image_tag 'toggle_check.png'
1042 1042 end
1043 1043 end
1044 1044
1045 1045 def context_menu(url)
1046 1046 unless @context_menu_included
1047 1047 content_for :header_tags do
1048 1048 javascript_include_tag('context_menu') +
1049 1049 stylesheet_link_tag('context_menu')
1050 1050 end
1051 1051 if l(:direction) == 'rtl'
1052 1052 content_for :header_tags do
1053 1053 stylesheet_link_tag('context_menu_rtl')
1054 1054 end
1055 1055 end
1056 1056 @context_menu_included = true
1057 1057 end
1058 1058 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1059 1059 end
1060 1060
1061 1061 def calendar_for(field_id)
1062 1062 include_calendar_headers_tags
1063 1063 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1064 1064 end
1065 1065
1066 1066 def include_calendar_headers_tags
1067 1067 unless @calendar_headers_tags_included
1068 1068 @calendar_headers_tags_included = true
1069 1069 content_for :header_tags do
1070 1070 start_of_week = Setting.start_of_week
1071 1071 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1072 1072 # Redmine uses 1..7 (monday..sunday) in settings and locales
1073 1073 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1074 1074 start_of_week = start_of_week.to_i % 7
1075 1075
1076 1076 tags = javascript_tag(
1077 1077 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1078 1078 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1079 1079 path_to_image('/images/calendar.png') +
1080 1080 "', showButtonPanel: true};")
1081 1081 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1082 1082 unless jquery_locale == 'en'
1083 1083 tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js")
1084 1084 end
1085 1085 tags
1086 1086 end
1087 1087 end
1088 1088 end
1089 1089
1090 1090 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1091 1091 # Examples:
1092 1092 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1093 1093 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1094 1094 #
1095 1095 def stylesheet_link_tag(*sources)
1096 1096 options = sources.last.is_a?(Hash) ? sources.pop : {}
1097 1097 plugin = options.delete(:plugin)
1098 1098 sources = sources.map do |source|
1099 1099 if plugin
1100 1100 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1101 1101 elsif current_theme && current_theme.stylesheets.include?(source)
1102 1102 current_theme.stylesheet_path(source)
1103 1103 else
1104 1104 source
1105 1105 end
1106 1106 end
1107 1107 super sources, options
1108 1108 end
1109 1109
1110 1110 # Overrides Rails' image_tag with themes and plugins support.
1111 1111 # Examples:
1112 1112 # image_tag('image.png') # => picks image.png from the current theme or defaults
1113 1113 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1114 1114 #
1115 1115 def image_tag(source, options={})
1116 1116 if plugin = options.delete(:plugin)
1117 1117 source = "/plugin_assets/#{plugin}/images/#{source}"
1118 1118 elsif current_theme && current_theme.images.include?(source)
1119 1119 source = current_theme.image_path(source)
1120 1120 end
1121 1121 super source, options
1122 1122 end
1123 1123
1124 1124 # Overrides Rails' javascript_include_tag with plugins support
1125 1125 # Examples:
1126 1126 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1127 1127 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1128 1128 #
1129 1129 def javascript_include_tag(*sources)
1130 1130 options = sources.last.is_a?(Hash) ? sources.pop : {}
1131 1131 if plugin = options.delete(:plugin)
1132 1132 sources = sources.map do |source|
1133 1133 if plugin
1134 1134 "/plugin_assets/#{plugin}/javascripts/#{source}"
1135 1135 else
1136 1136 source
1137 1137 end
1138 1138 end
1139 1139 end
1140 1140 super sources, options
1141 1141 end
1142 1142
1143 1143 def content_for(name, content = nil, &block)
1144 1144 @has_content ||= {}
1145 1145 @has_content[name] = true
1146 1146 super(name, content, &block)
1147 1147 end
1148 1148
1149 1149 def has_content?(name)
1150 1150 (@has_content && @has_content[name]) || false
1151 1151 end
1152 1152
1153 1153 def sidebar_content?
1154 1154 has_content?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1155 1155 end
1156 1156
1157 1157 def view_layouts_base_sidebar_hook_response
1158 1158 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1159 1159 end
1160 1160
1161 1161 def email_delivery_enabled?
1162 1162 !!ActionMailer::Base.perform_deliveries
1163 1163 end
1164 1164
1165 1165 # Returns the avatar image tag for the given +user+ if avatars are enabled
1166 1166 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1167 1167 def avatar(user, options = { })
1168 1168 if Setting.gravatar_enabled?
1169 1169 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1170 1170 email = nil
1171 1171 if user.respond_to?(:mail)
1172 1172 email = user.mail
1173 1173 elsif user.to_s =~ %r{<(.+?)>}
1174 1174 email = $1
1175 1175 end
1176 1176 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1177 1177 else
1178 1178 ''
1179 1179 end
1180 1180 end
1181 1181
1182 1182 def sanitize_anchor_name(anchor)
1183 1183 if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java'
1184 1184 anchor.gsub(%r{[^\p{Word}\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1185 1185 else
1186 1186 # TODO: remove when ruby1.8 is no longer supported
1187 1187 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1188 1188 end
1189 1189 end
1190 1190
1191 1191 # Returns the javascript tags that are included in the html layout head
1192 1192 def javascript_heads
1193 1193 tags = javascript_include_tag('jquery-1.8.3-ui-1.9.2-ujs-2.0.3', 'application')
1194 1194 unless User.current.pref.warn_on_leaving_unsaved == '0'
1195 1195 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1196 1196 end
1197 1197 tags
1198 1198 end
1199 1199
1200 1200 def favicon
1201 1201 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1202 1202 end
1203 1203
1204 1204 def robot_exclusion_tag
1205 1205 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1206 1206 end
1207 1207
1208 1208 # Returns true if arg is expected in the API response
1209 1209 def include_in_api_response?(arg)
1210 1210 unless @included_in_api_response
1211 1211 param = params[:include]
1212 1212 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1213 1213 @included_in_api_response.collect!(&:strip)
1214 1214 end
1215 1215 @included_in_api_response.include?(arg.to_s)
1216 1216 end
1217 1217
1218 1218 # Returns options or nil if nometa param or X-Redmine-Nometa header
1219 1219 # was set in the request
1220 1220 def api_meta(options)
1221 1221 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1222 1222 # compatibility mode for activeresource clients that raise
1223 1223 # an error when unserializing an array with attributes
1224 1224 nil
1225 1225 else
1226 1226 options
1227 1227 end
1228 1228 end
1229 1229
1230 1230 private
1231 1231
1232 1232 def wiki_helper
1233 1233 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1234 1234 extend helper
1235 1235 return self
1236 1236 end
1237 1237
1238 1238 def link_to_content_update(text, url_params = {}, html_options = {})
1239 1239 link_to(text, url_params, html_options)
1240 1240 end
1241 1241 end
@@ -1,344 +1,344
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 RedmineApp::Application.routes.draw do
19 19 root :to => 'welcome#index', :as => 'home'
20 20
21 21 match 'login', :to => 'account#login', :as => 'signin', :via => [:get, :post]
22 22 match 'logout', :to => 'account#logout', :as => 'signout', :via => [:get, :post]
23 23 match 'account/register', :to => 'account#register', :via => [:get, :post], :as => 'register'
24 24 match 'account/lost_password', :to => 'account#lost_password', :via => [:get, :post], :as => 'lost_password'
25 25 match 'account/activate', :to => 'account#activate', :via => :get
26 26
27 27 match '/news/preview', :controller => 'previews', :action => 'news', :as => 'preview_news', :via => [:get, :post, :put]
28 28 match '/issues/preview/new/:project_id', :to => 'previews#issue', :as => 'preview_new_issue', :via => [:get, :post, :put]
29 29 match '/issues/preview/edit/:id', :to => 'previews#issue', :as => 'preview_edit_issue', :via => [:get, :post, :put]
30 30 match '/issues/preview', :to => 'previews#issue', :as => 'preview_issue', :via => [:get, :post, :put]
31 31
32 32 match 'projects/:id/wiki', :to => 'wikis#edit', :via => :post
33 33 match 'projects/:id/wiki/destroy', :to => 'wikis#destroy', :via => [:get, :post]
34 34
35 35 match 'boards/:board_id/topics/new', :to => 'messages#new', :via => [:get, :post], :as => 'new_board_message'
36 36 get 'boards/:board_id/topics/:id', :to => 'messages#show', :as => 'board_message'
37 37 match 'boards/:board_id/topics/quote/:id', :to => 'messages#quote', :via => [:get, :post]
38 38 get 'boards/:board_id/topics/:id/edit', :to => 'messages#edit'
39 39
40 40 post 'boards/:board_id/topics/preview', :to => 'messages#preview', :as => 'preview_board_message'
41 41 post 'boards/:board_id/topics/:id/replies', :to => 'messages#reply'
42 42 post 'boards/:board_id/topics/:id/edit', :to => 'messages#edit'
43 43 post 'boards/:board_id/topics/:id/destroy', :to => 'messages#destroy'
44 44
45 45 # Misc issue routes. TODO: move into resources
46 46 match '/issues/auto_complete', :to => 'auto_completes#issues', :via => :get, :as => 'auto_complete_issues'
47 47 match '/issues/context_menu', :to => 'context_menus#issues', :as => 'issues_context_menu', :via => [:get, :post]
48 48 match '/issues/changes', :to => 'journals#index', :as => 'issue_changes', :via => :get
49 49 match '/issues/:id/quoted', :to => 'journals#new', :id => /\d+/, :via => :post, :as => 'quoted_issue'
50 50
51 51 match '/journals/diff/:id', :to => 'journals#diff', :id => /\d+/, :via => :get
52 52 match '/journals/edit/:id', :to => 'journals#edit', :id => /\d+/, :via => [:get, :post]
53 53
54 54 get '/projects/:project_id/issues/gantt', :to => 'gantts#show', :as => 'project_gantt'
55 55 get '/issues/gantt', :to => 'gantts#show'
56 56
57 57 get '/projects/:project_id/issues/calendar', :to => 'calendars#show', :as => 'project_calendar'
58 58 get '/issues/calendar', :to => 'calendars#show'
59 59
60 60 get 'projects/:id/issues/report', :to => 'reports#issue_report', :as => 'project_issues_report'
61 61 get 'projects/:id/issues/report/:detail', :to => 'reports#issue_report_details', :as => 'project_issues_report_details'
62 62
63 63 match 'my/account', :controller => 'my', :action => 'account', :via => [:get, :post]
64 64 match 'my/account/destroy', :controller => 'my', :action => 'destroy', :via => [:get, :post]
65 65 match 'my/page', :controller => 'my', :action => 'page', :via => :get
66 66 match 'my', :controller => 'my', :action => 'index', :via => :get # Redirects to my/page
67 67 match 'my/reset_rss_key', :controller => 'my', :action => 'reset_rss_key', :via => :post
68 68 match 'my/reset_api_key', :controller => 'my', :action => 'reset_api_key', :via => :post
69 69 match 'my/password', :controller => 'my', :action => 'password', :via => [:get, :post]
70 70 match 'my/page_layout', :controller => 'my', :action => 'page_layout', :via => :get
71 71 match 'my/add_block', :controller => 'my', :action => 'add_block', :via => :post
72 72 match 'my/remove_block', :controller => 'my', :action => 'remove_block', :via => :post
73 73 match 'my/order_blocks', :controller => 'my', :action => 'order_blocks', :via => :post
74 74
75 75 resources :users
76 76 match 'users/:id/memberships/:membership_id', :to => 'users#edit_membership', :via => :put, :as => 'user_membership'
77 77 match 'users/:id/memberships/:membership_id', :to => 'users#destroy_membership', :via => :delete
78 78 match 'users/:id/memberships', :to => 'users#edit_membership', :via => :post, :as => 'user_memberships'
79 79
80 80 match 'watchers/new', :controller=> 'watchers', :action => 'new', :via => :get
81 81 match 'watchers', :controller=> 'watchers', :action => 'create', :via => :post
82 82 match 'watchers/append', :controller=> 'watchers', :action => 'append', :via => :post
83 83 match 'watchers/destroy', :controller=> 'watchers', :action => 'destroy', :via => :post
84 84 match 'watchers/watch', :controller=> 'watchers', :action => 'watch', :via => :post
85 85 match 'watchers/unwatch', :controller=> 'watchers', :action => 'unwatch', :via => :post
86 86 match 'watchers/autocomplete_for_user', :controller=> 'watchers', :action => 'autocomplete_for_user', :via => :get
87 87
88 88 resources :projects do
89 89 member do
90 90 get 'settings(/:tab)', :action => 'settings', :as => 'settings'
91 91 post 'modules'
92 92 post 'archive'
93 93 post 'unarchive'
94 94 post 'close'
95 95 post 'reopen'
96 96 match 'copy', :via => [:get, :post]
97 97 end
98 98
99 99 resources :memberships, :shallow => true, :controller => 'members', :only => [:index, :show, :new, :create, :update, :destroy] do
100 100 collection do
101 101 get 'autocomplete'
102 102 end
103 103 end
104 104
105 105 resource :enumerations, :controller => 'project_enumerations', :only => [:update, :destroy]
106 106
107 107 get 'issues/:copy_from/copy', :to => 'issues#new', :as => 'copy_issue'
108 108 resources :issues, :only => [:index, :new, :create] do
109 109 resources :time_entries, :controller => 'timelog' do
110 110 collection do
111 111 get 'report'
112 112 end
113 113 end
114 114 end
115 115 # issue form update
116 116 match 'issues/new', :controller => 'issues', :action => 'new', :via => [:put, :post], :as => 'issue_form'
117 117
118 118 resources :files, :only => [:index, :new, :create]
119 119
120 120 resources :versions, :except => [:index, :show, :edit, :update, :destroy] do
121 121 collection do
122 122 put 'close_completed'
123 123 end
124 124 end
125 125 get 'versions.:format', :to => 'versions#index'
126 126 get 'roadmap', :to => 'versions#index', :format => false
127 127 get 'versions', :to => 'versions#index'
128 128
129 129 resources :news, :except => [:show, :edit, :update, :destroy]
130 130 resources :time_entries, :controller => 'timelog' do
131 131 get 'report', :on => :collection
132 132 end
133 133 resources :queries, :only => [:new, :create]
134 134 resources :issue_categories, :shallow => true
135 135 resources :documents, :except => [:show, :edit, :update, :destroy]
136 136 resources :boards
137 137 resources :repositories, :shallow => true, :except => [:index, :show] do
138 138 member do
139 139 match 'committers', :via => [:get, :post]
140 140 end
141 141 end
142 142
143 143 match 'wiki/index', :controller => 'wiki', :action => 'index', :via => :get
144 144 resources :wiki, :except => [:index, :new, :create], :as => 'wiki_page' do
145 145 member do
146 146 get 'rename'
147 147 post 'rename'
148 148 get 'history'
149 149 get 'diff'
150 150 match 'preview', :via => [:post, :put]
151 151 post 'protect'
152 152 post 'add_attachment'
153 153 end
154 154 collection do
155 155 get 'export'
156 156 get 'date_index'
157 157 end
158 158 end
159 159 match 'wiki', :controller => 'wiki', :action => 'show', :via => :get
160 160 get 'wiki/:id/:version', :to => 'wiki#show', :constraints => {:version => /\d+/}
161 161 delete 'wiki/:id/:version', :to => 'wiki#destroy_version'
162 162 get 'wiki/:id/:version/annotate', :to => 'wiki#annotate'
163 163 get 'wiki/:id/:version/diff', :to => 'wiki#diff'
164 164 end
165 165
166 166 resources :issues do
167 167 collection do
168 168 match 'bulk_edit', :via => [:get, :post]
169 169 post 'bulk_update'
170 170 end
171 171 resources :time_entries, :controller => 'timelog' do
172 172 collection do
173 173 get 'report'
174 174 end
175 175 end
176 176 resources :relations, :shallow => true, :controller => 'issue_relations', :only => [:index, :show, :create, :destroy]
177 177 end
178 178 match '/issues', :controller => 'issues', :action => 'destroy', :via => :delete
179 179
180 180 resources :queries, :except => [:show]
181 181
182 182 resources :news, :only => [:index, :show, :edit, :update, :destroy]
183 183 match '/news/:id/comments', :to => 'comments#create', :via => :post
184 184 match '/news/:id/comments/:comment_id', :to => 'comments#destroy', :via => :delete
185 185
186 186 resources :versions, :only => [:show, :edit, :update, :destroy] do
187 187 post 'status_by', :on => :member
188 188 end
189 189
190 190 resources :documents, :only => [:show, :edit, :update, :destroy] do
191 191 post 'add_attachment', :on => :member
192 192 end
193 193
194 194 match '/time_entries/context_menu', :to => 'context_menus#time_entries', :as => :time_entries_context_menu, :via => [:get, :post]
195 195
196 196 resources :time_entries, :controller => 'timelog', :except => :destroy do
197 197 collection do
198 198 get 'report'
199 199 get 'bulk_edit'
200 200 post 'bulk_update'
201 201 end
202 202 end
203 203 match '/time_entries/:id', :to => 'timelog#destroy', :via => :delete, :id => /\d+/
204 204 # TODO: delete /time_entries for bulk deletion
205 205 match '/time_entries/destroy', :to => 'timelog#destroy', :via => :delete
206 206
207 207 get 'projects/:id/activity', :to => 'activities#index'
208 208 get 'projects/:id/activity.:format', :to => 'activities#index'
209 209 get 'activity', :to => 'activities#index'
210 210
211 211 # repositories routes
212 212 get 'projects/:id/repository/:repository_id/statistics', :to => 'repositories#stats'
213 213 get 'projects/:id/repository/:repository_id/graph', :to => 'repositories#graph'
214 214
215 215 get 'projects/:id/repository/:repository_id/changes(/*path(.:ext))',
216 216 :to => 'repositories#changes'
217 217
218 218 get 'projects/:id/repository/:repository_id/revisions/:rev', :to => 'repositories#revision'
219 219 get 'projects/:id/repository/:repository_id/revision', :to => 'repositories#revision'
220 220 post 'projects/:id/repository/:repository_id/revisions/:rev/issues', :to => 'repositories#add_related_issue'
221 221 delete 'projects/:id/repository/:repository_id/revisions/:rev/issues/:issue_id', :to => 'repositories#remove_related_issue'
222 222 get 'projects/:id/repository/:repository_id/revisions', :to => 'repositories#revisions'
223 223 get 'projects/:id/repository/:repository_id/revisions/:rev/:action(/*path(.:ext))',
224 224 :controller => 'repositories',
225 225 :format => false,
226 226 :constraints => {
227 227 :action => /(browse|show|entry|raw|annotate|diff)/,
228 228 :rev => /[a-z0-9\.\-_]+/
229 229 }
230 230
231 231 get 'projects/:id/repository/statistics', :to => 'repositories#stats'
232 232 get 'projects/:id/repository/graph', :to => 'repositories#graph'
233 233
234 234 get 'projects/:id/repository/changes(/*path(.:ext))',
235 235 :to => 'repositories#changes'
236 236
237 237 get 'projects/:id/repository/revisions', :to => 'repositories#revisions'
238 238 get 'projects/:id/repository/revisions/:rev', :to => 'repositories#revision'
239 239 get 'projects/:id/repository/revision', :to => 'repositories#revision'
240 240 post 'projects/:id/repository/revisions/:rev/issues', :to => 'repositories#add_related_issue'
241 241 delete 'projects/:id/repository/revisions/:rev/issues/:issue_id', :to => 'repositories#remove_related_issue'
242 242 get 'projects/:id/repository/revisions/:rev/:action(/*path(.:ext))',
243 243 :controller => 'repositories',
244 244 :format => false,
245 245 :constraints => {
246 246 :action => /(browse|show|entry|raw|annotate|diff)/,
247 247 :rev => /[a-z0-9\.\-_]+/
248 248 }
249 249 get 'projects/:id/repository/:repository_id/:action(/*path(.:ext))',
250 250 :controller => 'repositories',
251 251 :action => /(browse|show|entry|raw|changes|annotate|diff)/
252 252 get 'projects/:id/repository/:action(/*path(.:ext))',
253 253 :controller => 'repositories',
254 254 :action => /(browse|show|entry|raw|changes|annotate|diff)/
255 255
256 256 get 'projects/:id/repository/:repository_id', :to => 'repositories#show', :path => nil
257 257 get 'projects/:id/repository', :to => 'repositories#show', :path => nil
258 258
259 259 # additional routes for having the file name at the end of url
260 260 get 'attachments/:id/:filename', :to => 'attachments#show', :id => /\d+/, :filename => /.*/, :as => 'named_attachment'
261 261 get 'attachments/download/:id/:filename', :to => 'attachments#download', :id => /\d+/, :filename => /.*/, :as => 'download_named_attachment'
262 262 get 'attachments/download/:id', :to => 'attachments#download', :id => /\d+/
263 get 'attachments/thumbnail/:id(/:size)', :to => 'attachments#thumbnail', :id => /\d+/, :size => /\d+/
263 get 'attachments/thumbnail/:id(/:size)', :to => 'attachments#thumbnail', :id => /\d+/, :size => /\d+/, :as => 'thumbnail'
264 264 resources :attachments, :only => [:show, :destroy]
265 265
266 266 resources :groups do
267 267 member do
268 268 get 'autocomplete_for_user'
269 269 end
270 270 end
271 271
272 272 match 'groups/:id/users', :controller => 'groups', :action => 'add_users', :id => /\d+/, :via => :post, :as => 'group_users'
273 273 match 'groups/:id/users/:user_id', :controller => 'groups', :action => 'remove_user', :id => /\d+/, :via => :delete, :as => 'group_user'
274 274 match 'groups/destroy_membership/:id', :controller => 'groups', :action => 'destroy_membership', :id => /\d+/, :via => :post
275 275 match 'groups/edit_membership/:id', :controller => 'groups', :action => 'edit_membership', :id => /\d+/, :via => :post
276 276
277 277 resources :trackers, :except => :show do
278 278 collection do
279 279 match 'fields', :via => [:get, :post]
280 280 end
281 281 end
282 282 resources :issue_statuses, :except => :show do
283 283 collection do
284 284 post 'update_issue_done_ratio'
285 285 end
286 286 end
287 287 resources :custom_fields, :except => :show
288 288 resources :roles do
289 289 collection do
290 290 match 'permissions', :via => [:get, :post]
291 291 end
292 292 end
293 293 resources :enumerations, :except => :show
294 294 match 'enumerations/:type', :to => 'enumerations#index', :via => :get
295 295
296 296 get 'projects/:id/search', :controller => 'search', :action => 'index'
297 297 get 'search', :controller => 'search', :action => 'index'
298 298
299 299 match 'mail_handler', :controller => 'mail_handler', :action => 'index', :via => :post
300 300
301 301 match 'admin', :controller => 'admin', :action => 'index', :via => :get
302 302 match 'admin/projects', :controller => 'admin', :action => 'projects', :via => :get
303 303 match 'admin/plugins', :controller => 'admin', :action => 'plugins', :via => :get
304 304 match 'admin/info', :controller => 'admin', :action => 'info', :via => :get
305 305 match 'admin/test_email', :controller => 'admin', :action => 'test_email', :via => :get
306 306 match 'admin/default_configuration', :controller => 'admin', :action => 'default_configuration', :via => :post
307 307
308 308 resources :auth_sources do
309 309 member do
310 310 get 'test_connection', :as => 'try_connection'
311 311 end
312 312 collection do
313 313 get 'autocomplete_for_new_user'
314 314 end
315 315 end
316 316
317 317 match 'workflows', :controller => 'workflows', :action => 'index', :via => :get
318 318 match 'workflows/edit', :controller => 'workflows', :action => 'edit', :via => [:get, :post]
319 319 match 'workflows/permissions', :controller => 'workflows', :action => 'permissions', :via => [:get, :post]
320 320 match 'workflows/copy', :controller => 'workflows', :action => 'copy', :via => [:get, :post]
321 321 match 'settings', :controller => 'settings', :action => 'index', :via => :get
322 322 match 'settings/edit', :controller => 'settings', :action => 'edit', :via => [:get, :post]
323 323 match 'settings/plugin/:id', :controller => 'settings', :action => 'plugin', :via => [:get, :post], :as => 'plugin_settings'
324 324
325 325 match 'sys/projects', :to => 'sys#projects', :via => :get
326 326 match 'sys/projects/:id/repository', :to => 'sys#create_project_repository', :via => :post
327 327 match 'sys/fetch_changesets', :to => 'sys#fetch_changesets', :via => :get
328 328
329 329 match 'uploads', :to => 'attachments#upload', :via => :post
330 330
331 331 get 'robots.txt', :to => 'welcome#robots'
332 332
333 333 Dir.glob File.expand_path("plugins/*", Rails.root) do |plugin_dir|
334 334 file = File.join(plugin_dir, "config/routes.rb")
335 335 if File.exists?(file)
336 336 begin
337 337 instance_eval File.read(file)
338 338 rescue Exception => e
339 339 puts "An error occurred while loading the routes definition of #{File.basename(plugin_dir)} plugin (#{file}): #{e.message}."
340 340 exit 1
341 341 end
342 342 end
343 343 end
344 344 end
@@ -1,1178 +1,1184
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" title="This is a logo" alt="This is a logo" />',
136 136 'Inline image: !logo.GIF!' => 'Inline image: <img src="/attachments/download/3" 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" 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 + '" alt="" />',
187 187 'Inline image: !testtest.jpeg!' =>
188 188 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '" alt="" />',
189 189 'Inline image: !testtest.jpe!' =>
190 190 'Inline image: <img src="/attachments/download/' + a3.id.to_s + '" alt="" />',
191 191 'Inline image: !testtest.bmp!' =>
192 192 'Inline image: <img src="/attachments/download/' + a4.id.to_s + '" 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 + '" alt="" />',
216 216 'Inline image: !Testfile.PNG!' =>
217 217 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '" 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 attachment_link = link_to('error281.txt', {:controller => 'attachments', :action => 'download', :id => '1'}, :class => 'attachment')
553 553 to_test = {
554 554 'attachment:error281.txt' => attachment_link
555 555 }
556 556 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => Issue.find(3).attachments), "#{text} failed" }
557 557 end
558 558
559 559 def test_wiki_links
560 560 to_test = {
561 561 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
562 562 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
563 563 # title content should be formatted
564 564 '[[Another page|With _styled_ *title*]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">With <em>styled</em> <strong>title</strong></a>',
565 565 '[[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>',
566 566 # link with anchor
567 567 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
568 568 '[[Another page#anchor|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page#anchor" class="wiki-page">Page</a>',
569 569 # UTF8 anchor
570 570 '[[Another_page#ВСст|ВСст]]' => %|<a href="/projects/ecookbook/wiki/Another_page##{CGI.escape 'ВСст'}" class="wiki-page">ВСст</a>|,
571 571 # page that doesn't exist
572 572 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
573 573 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">404</a>',
574 574 # link to another project wiki
575 575 '[[onlinestore:]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">onlinestore</a>',
576 576 '[[onlinestore:|Wiki]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">Wiki</a>',
577 577 '[[onlinestore:Start page]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Start page</a>',
578 578 '[[onlinestore:Start page|Text]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Text</a>',
579 579 '[[onlinestore:Unknown page]]' => '<a href="/projects/onlinestore/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
580 580 # striked through link
581 581 '-[[Another page|Page]]-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a></del>',
582 582 '-[[Another page|Page]] link-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a> link</del>',
583 583 # escaping
584 584 '![[Another page|Page]]' => '[[Another page|Page]]',
585 585 # project does not exist
586 586 '[[unknowproject:Start]]' => '[[unknowproject:Start]]',
587 587 '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]',
588 588 }
589 589
590 590 @project = Project.find(1)
591 591 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
592 592 end
593 593
594 594 def test_wiki_links_within_local_file_generation_context
595 595
596 596 to_test = {
597 597 # link to a page
598 598 '[[CookBook documentation]]' => '<a href="CookBook_documentation.html" class="wiki-page">CookBook documentation</a>',
599 599 '[[CookBook documentation|documentation]]' => '<a href="CookBook_documentation.html" class="wiki-page">documentation</a>',
600 600 '[[CookBook documentation#One-section]]' => '<a href="CookBook_documentation.html#One-section" class="wiki-page">CookBook documentation</a>',
601 601 '[[CookBook documentation#One-section|documentation]]' => '<a href="CookBook_documentation.html#One-section" class="wiki-page">documentation</a>',
602 602 # page that doesn't exist
603 603 '[[Unknown page]]' => '<a href="Unknown_page.html" class="wiki-page new">Unknown page</a>',
604 604 '[[Unknown page|404]]' => '<a href="Unknown_page.html" class="wiki-page new">404</a>',
605 605 '[[Unknown page#anchor]]' => '<a href="Unknown_page.html#anchor" class="wiki-page new">Unknown page</a>',
606 606 '[[Unknown page#anchor|404]]' => '<a href="Unknown_page.html#anchor" class="wiki-page new">404</a>',
607 607 }
608 608
609 609 @project = Project.find(1)
610 610
611 611 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :local) }
612 612 end
613 613
614 614 def test_wiki_links_within_wiki_page_context
615 615
616 616 page = WikiPage.find_by_title('Another_page' )
617 617
618 618 to_test = {
619 619 # link to another page
620 620 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
621 621 '[[CookBook documentation|documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">documentation</a>',
622 622 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
623 623 '[[CookBook documentation#One-section|documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">documentation</a>',
624 624 # link to the current page
625 625 '[[Another page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Another page</a>',
626 626 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
627 627 '[[Another page#anchor]]' => '<a href="#anchor" class="wiki-page">Another page</a>',
628 628 '[[Another page#anchor|Page]]' => '<a href="#anchor" class="wiki-page">Page</a>',
629 629 # page that doesn't exist
630 630 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page" class="wiki-page new">Unknown page</a>',
631 631 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page" class="wiki-page new">404</a>',
632 632 '[[Unknown page#anchor]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor" class="wiki-page new">Unknown page</a>',
633 633 '[[Unknown page#anchor|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor" class="wiki-page new">404</a>',
634 634 }
635 635
636 636 @project = Project.find(1)
637 637
638 638 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(WikiContent.new( :text => text, :page => page ), :text) }
639 639 end
640 640
641 641 def test_wiki_links_anchor_option_should_prepend_page_title_to_href
642 642
643 643 to_test = {
644 644 # link to a page
645 645 '[[CookBook documentation]]' => '<a href="#CookBook_documentation" class="wiki-page">CookBook documentation</a>',
646 646 '[[CookBook documentation|documentation]]' => '<a href="#CookBook_documentation" class="wiki-page">documentation</a>',
647 647 '[[CookBook documentation#One-section]]' => '<a href="#CookBook_documentation_One-section" class="wiki-page">CookBook documentation</a>',
648 648 '[[CookBook documentation#One-section|documentation]]' => '<a href="#CookBook_documentation_One-section" class="wiki-page">documentation</a>',
649 649 # page that doesn't exist
650 650 '[[Unknown page]]' => '<a href="#Unknown_page" class="wiki-page new">Unknown page</a>',
651 651 '[[Unknown page|404]]' => '<a href="#Unknown_page" class="wiki-page new">404</a>',
652 652 '[[Unknown page#anchor]]' => '<a href="#Unknown_page_anchor" class="wiki-page new">Unknown page</a>',
653 653 '[[Unknown page#anchor|404]]' => '<a href="#Unknown_page_anchor" class="wiki-page new">404</a>',
654 654 }
655 655
656 656 @project = Project.find(1)
657 657
658 658 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :anchor) }
659 659 end
660 660
661 661 def test_html_tags
662 662 to_test = {
663 663 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
664 664 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
665 665 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
666 666 # do not escape pre/code tags
667 667 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
668 668 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
669 669 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
670 670 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
671 671 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
672 672 # remove attributes except class
673 673 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
674 674 '<pre class="foo">some text</pre>' => '<pre class="foo">some text</pre>',
675 675 "<pre class='foo bar'>some text</pre>" => "<pre class='foo bar'>some text</pre>",
676 676 '<pre class="foo bar">some text</pre>' => '<pre class="foo bar">some text</pre>',
677 677 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
678 678 # xss
679 679 '<pre><code class=""onmouseover="alert(1)">text</code></pre>' => '<pre><code>text</code></pre>',
680 680 '<pre class=""onmouseover="alert(1)">text</pre>' => '<pre>text</pre>',
681 681 }
682 682 to_test.each { |text, result| assert_equal result, textilizable(text) }
683 683 end
684 684
685 685 def test_allowed_html_tags
686 686 to_test = {
687 687 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
688 688 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
689 689 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
690 690 }
691 691 to_test.each { |text, result| assert_equal result, textilizable(text) }
692 692 end
693 693
694 694 def test_pre_tags
695 695 raw = <<-RAW
696 696 Before
697 697
698 698 <pre>
699 699 <prepared-statement-cache-size>32</prepared-statement-cache-size>
700 700 </pre>
701 701
702 702 After
703 703 RAW
704 704
705 705 expected = <<-EXPECTED
706 706 <p>Before</p>
707 707 <pre>
708 708 &lt;prepared-statement-cache-size&gt;32&lt;/prepared-statement-cache-size&gt;
709 709 </pre>
710 710 <p>After</p>
711 711 EXPECTED
712 712
713 713 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
714 714 end
715 715
716 716 def test_pre_content_should_not_parse_wiki_and_redmine_links
717 717 raw = <<-RAW
718 718 [[CookBook documentation]]
719 719
720 720 #1
721 721
722 722 <pre>
723 723 [[CookBook documentation]]
724 724
725 725 #1
726 726 </pre>
727 727 RAW
728 728
729 729 expected = <<-EXPECTED
730 730 <p><a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a></p>
731 731 <p><a href="/issues/1" class="issue status-1 priority-4 priority-lowest" title="Can&#x27;t print recipes (New)">#1</a></p>
732 732 <pre>
733 733 [[CookBook documentation]]
734 734
735 735 #1
736 736 </pre>
737 737 EXPECTED
738 738
739 739 @project = Project.find(1)
740 740 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
741 741 end
742 742
743 743 def test_non_closing_pre_blocks_should_be_closed
744 744 raw = <<-RAW
745 745 <pre><code>
746 746 RAW
747 747
748 748 expected = <<-EXPECTED
749 749 <pre><code>
750 750 </code></pre>
751 751 EXPECTED
752 752
753 753 @project = Project.find(1)
754 754 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
755 755 end
756 756
757 757 def test_syntax_highlight
758 758 raw = <<-RAW
759 759 <pre><code class="ruby">
760 760 # Some ruby code here
761 761 </code></pre>
762 762 RAW
763 763
764 764 expected = <<-EXPECTED
765 765 <pre><code class="ruby syntaxhl"><span class=\"CodeRay\"><span class="comment"># Some ruby code here</span></span>
766 766 </code></pre>
767 767 EXPECTED
768 768
769 769 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
770 770 end
771 771
772 772 def test_to_path_param
773 773 assert_equal 'test1/test2', to_path_param('test1/test2')
774 774 assert_equal 'test1/test2', to_path_param('/test1/test2/')
775 775 assert_equal 'test1/test2', to_path_param('//test1/test2/')
776 776 assert_equal nil, to_path_param('/')
777 777 end
778 778
779 779 def test_wiki_links_in_tables
780 780 to_test = {"|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" =>
781 781 '<tr><td><a href="/projects/ecookbook/wiki/Page" class="wiki-page new">Link title</a></td>' +
782 782 '<td><a href="/projects/ecookbook/wiki/Other_Page" class="wiki-page new">Other title</a></td>' +
783 783 '</tr><tr><td>Cell 21</td><td><a href="/projects/ecookbook/wiki/Last_page" class="wiki-page new">Last page</a></td></tr>'
784 784 }
785 785 @project = Project.find(1)
786 786 to_test.each { |text, result| assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '') }
787 787 end
788 788
789 789 def test_text_formatting
790 790 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
791 791 '(_text within parentheses_)' => '(<em>text within parentheses</em>)',
792 792 'a *Humane Web* Text Generator' => 'a <strong>Humane Web</strong> Text Generator',
793 793 '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>',
794 794 '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',
795 795 }
796 796 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
797 797 end
798 798
799 799 def test_wiki_horizontal_rule
800 800 assert_equal '<hr />', textilizable('---')
801 801 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
802 802 end
803 803
804 804 def test_footnotes
805 805 raw = <<-RAW
806 806 This is some text[1].
807 807
808 808 fn1. This is the foot note
809 809 RAW
810 810
811 811 expected = <<-EXPECTED
812 812 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
813 813 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
814 814 EXPECTED
815 815
816 816 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
817 817 end
818 818
819 819 def test_headings
820 820 raw = 'h1. Some heading'
821 821 expected = %|<a name="Some-heading"></a>\n<h1 >Some heading<a href="#Some-heading" class="wiki-anchor">&para;</a></h1>|
822 822
823 823 assert_equal expected, textilizable(raw)
824 824 end
825 825
826 826 def test_headings_with_special_chars
827 827 # This test makes sure that the generated anchor names match the expected
828 828 # ones even if the heading text contains unconventional characters
829 829 raw = 'h1. Some heading related to version 0.5'
830 830 anchor = sanitize_anchor_name("Some-heading-related-to-version-0.5")
831 831 expected = %|<a name="#{anchor}"></a>\n<h1 >Some heading related to version 0.5<a href="##{anchor}" class="wiki-anchor">&para;</a></h1>|
832 832
833 833 assert_equal expected, textilizable(raw)
834 834 end
835 835
836 836 def test_headings_in_wiki_single_page_export_should_be_prepended_with_page_title
837 837 page = WikiPage.new( :title => 'Page Title', :wiki_id => 1 )
838 838 content = WikiContent.new( :text => 'h1. Some heading', :page => page )
839 839
840 840 expected = %|<a name="Page_Title_Some-heading"></a>\n<h1 >Some heading<a href="#Page_Title_Some-heading" class="wiki-anchor">&para;</a></h1>|
841 841
842 842 assert_equal expected, textilizable(content, :text, :wiki_links => :anchor )
843 843 end
844 844
845 845 def test_table_of_content
846 846 raw = <<-RAW
847 847 {{toc}}
848 848
849 849 h1. Title
850 850
851 851 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
852 852
853 853 h2. Subtitle with a [[Wiki]] link
854 854
855 855 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
856 856
857 857 h2. Subtitle with [[Wiki|another Wiki]] link
858 858
859 859 h2. Subtitle with %{color:red}red text%
860 860
861 861 <pre>
862 862 some code
863 863 </pre>
864 864
865 865 h3. Subtitle with *some* _modifiers_
866 866
867 867 h3. Subtitle with @inline code@
868 868
869 869 h1. Another title
870 870
871 871 h3. An "Internet link":http://www.redmine.org/ inside subtitle
872 872
873 873 h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
874 874
875 875 RAW
876 876
877 877 expected = '<ul class="toc">' +
878 878 '<li><a href="#Title">Title</a>' +
879 879 '<ul>' +
880 880 '<li><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
881 881 '<li><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
882 882 '<li><a href="#Subtitle-with-red-text">Subtitle with red text</a>' +
883 883 '<ul>' +
884 884 '<li><a href="#Subtitle-with-some-modifiers">Subtitle with some modifiers</a></li>' +
885 885 '<li><a href="#Subtitle-with-inline-code">Subtitle with inline code</a></li>' +
886 886 '</ul>' +
887 887 '</li>' +
888 888 '</ul>' +
889 889 '</li>' +
890 890 '<li><a href="#Another-title">Another title</a>' +
891 891 '<ul>' +
892 892 '<li>' +
893 893 '<ul>' +
894 894 '<li><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
895 895 '</ul>' +
896 896 '</li>' +
897 897 '<li><a href="#Project-Name">Project Name</a></li>' +
898 898 '</ul>' +
899 899 '</li>' +
900 900 '</ul>'
901 901
902 902 @project = Project.find(1)
903 903 assert textilizable(raw).gsub("\n", "").include?(expected)
904 904 end
905 905
906 906 def test_table_of_content_should_generate_unique_anchors
907 907 raw = <<-RAW
908 908 {{toc}}
909 909
910 910 h1. Title
911 911
912 912 h2. Subtitle
913 913
914 914 h2. Subtitle
915 915 RAW
916 916
917 917 expected = '<ul class="toc">' +
918 918 '<li><a href="#Title">Title</a>' +
919 919 '<ul>' +
920 920 '<li><a href="#Subtitle">Subtitle</a></li>' +
921 921 '<li><a href="#Subtitle-2">Subtitle</a></li>'
922 922 '</ul>'
923 923 '</li>' +
924 924 '</ul>'
925 925
926 926 @project = Project.find(1)
927 927 result = textilizable(raw).gsub("\n", "")
928 928 assert_include expected, result
929 929 assert_include '<a name="Subtitle">', result
930 930 assert_include '<a name="Subtitle-2">', result
931 931 end
932 932
933 933 def test_table_of_content_should_contain_included_page_headings
934 934 raw = <<-RAW
935 935 {{toc}}
936 936
937 937 h1. Included
938 938
939 939 {{include(Child_1)}}
940 940 RAW
941 941
942 942 expected = '<ul class="toc">' +
943 943 '<li><a href="#Included">Included</a></li>' +
944 944 '<li><a href="#Child-page-1">Child page 1</a></li>' +
945 945 '</ul>'
946 946
947 947 @project = Project.find(1)
948 948 assert textilizable(raw).gsub("\n", "").include?(expected)
949 949 end
950 950
951 951 def test_section_edit_links
952 952 raw = <<-RAW
953 953 h1. Title
954 954
955 955 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
956 956
957 957 h2. Subtitle with a [[Wiki]] link
958 958
959 959 h2. Subtitle with *some* _modifiers_
960 960
961 961 h2. Subtitle with @inline code@
962 962
963 963 <pre>
964 964 some code
965 965
966 966 h2. heading inside pre
967 967
968 968 <h2>html heading inside pre</h2>
969 969 </pre>
970 970
971 971 h2. Subtitle after pre tag
972 972 RAW
973 973
974 974 @project = Project.find(1)
975 975 set_language_if_valid 'en'
976 976 result = textilizable(raw, :edit_section_links => {:controller => 'wiki', :action => 'edit', :project_id => '1', :id => 'Test'}).gsub("\n", "")
977 977
978 978 # heading that contains inline code
979 979 assert_match Regexp.new('<div class="contextual" title="Edit this section">' +
980 980 '<a href="/projects/1/wiki/Test/edit\?section=4"><img alt="Edit" src="/images/edit.png(\?\d+)?" /></a></div>' +
981 981 '<a name="Subtitle-with-inline-code"></a>' +
982 982 '<h2 >Subtitle with <code>inline code</code><a href="#Subtitle-with-inline-code" class="wiki-anchor">&para;</a></h2>'),
983 983 result
984 984
985 985 # last heading
986 986 assert_match Regexp.new('<div class="contextual" title="Edit this section">' +
987 987 '<a href="/projects/1/wiki/Test/edit\?section=5"><img alt="Edit" src="/images/edit.png(\?\d+)?" /></a></div>' +
988 988 '<a name="Subtitle-after-pre-tag"></a>' +
989 989 '<h2 >Subtitle after pre tag<a href="#Subtitle-after-pre-tag" class="wiki-anchor">&para;</a></h2>'),
990 990 result
991 991 end
992 992
993 993 def test_default_formatter
994 994 with_settings :text_formatting => 'unknown' do
995 995 text = 'a *link*: http://www.example.net/'
996 996 assert_equal '<p>a *link*: <a class="external" href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
997 997 end
998 998 end
999 999
1000 1000 def test_due_date_distance_in_words
1001 1001 to_test = { Date.today => 'Due in 0 days',
1002 1002 Date.today + 1 => 'Due in 1 day',
1003 1003 Date.today + 100 => 'Due in about 3 months',
1004 1004 Date.today + 20000 => 'Due in over 54 years',
1005 1005 Date.today - 1 => '1 day late',
1006 1006 Date.today - 100 => 'about 3 months late',
1007 1007 Date.today - 20000 => 'over 54 years late',
1008 1008 }
1009 1009 ::I18n.locale = :en
1010 1010 to_test.each do |date, expected|
1011 1011 assert_equal expected, due_date_distance_in_words(date)
1012 1012 end
1013 1013 end
1014 1014
1015 1015 def test_avatar_enabled
1016 1016 with_settings :gravatar_enabled => '1' do
1017 1017 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
1018 1018 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
1019 1019 # Default size is 50
1020 1020 assert avatar('jsmith <jsmith@somenet.foo>').include?('size=50')
1021 1021 assert avatar('jsmith <jsmith@somenet.foo>', :size => 24).include?('size=24')
1022 1022 # Non-avatar options should be considered html options
1023 1023 assert avatar('jsmith <jsmith@somenet.foo>', :title => 'John Smith').include?('title="John Smith"')
1024 1024 # The default class of the img tag should be gravatar
1025 1025 assert avatar('jsmith <jsmith@somenet.foo>').include?('class="gravatar"')
1026 1026 assert !avatar('jsmith <jsmith@somenet.foo>', :class => 'picture').include?('class="gravatar"')
1027 1027 assert_nil avatar('jsmith')
1028 1028 assert_nil avatar(nil)
1029 1029 end
1030 1030 end
1031 1031
1032 1032 def test_avatar_disabled
1033 1033 with_settings :gravatar_enabled => '0' do
1034 1034 assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
1035 1035 end
1036 1036 end
1037 1037
1038 1038 def test_link_to_user
1039 1039 user = User.find(2)
1040 1040 assert_equal '<a href="/users/2" class="user active">John Smith</a>', link_to_user(user)
1041 1041 end
1042 1042
1043 1043 def test_link_to_user_should_not_link_to_locked_user
1044 1044 with_current_user nil do
1045 1045 user = User.find(5)
1046 1046 assert user.locked?
1047 1047 assert_equal 'Dave2 Lopper2', link_to_user(user)
1048 1048 end
1049 1049 end
1050 1050
1051 1051 def test_link_to_user_should_link_to_locked_user_if_current_user_is_admin
1052 1052 with_current_user User.find(1) do
1053 1053 user = User.find(5)
1054 1054 assert user.locked?
1055 1055 assert_equal '<a href="/users/5" class="user locked">Dave2 Lopper2</a>', link_to_user(user)
1056 1056 end
1057 1057 end
1058 1058
1059 1059 def test_link_to_user_should_not_link_to_anonymous
1060 1060 user = User.anonymous
1061 1061 assert user.anonymous?
1062 1062 t = link_to_user(user)
1063 1063 assert_equal ::I18n.t(:label_user_anonymous), t
1064 1064 end
1065 1065
1066 1066 def test_link_to_attachment
1067 1067 a = Attachment.find(3)
1068 1068 assert_equal '<a href="/attachments/3/logo.gif">logo.gif</a>',
1069 1069 link_to_attachment(a)
1070 1070 assert_equal '<a href="/attachments/3/logo.gif">Text</a>',
1071 1071 link_to_attachment(a, :text => 'Text')
1072 1072 assert_equal '<a href="/attachments/3/logo.gif" class="foo">logo.gif</a>',
1073 1073 link_to_attachment(a, :class => 'foo')
1074 1074 assert_equal '<a href="/attachments/download/3/logo.gif">logo.gif</a>',
1075 1075 link_to_attachment(a, :download => true)
1076 1076 assert_equal '<a href="http://test.host/attachments/3/logo.gif">logo.gif</a>',
1077 1077 link_to_attachment(a, :only_path => false)
1078 1078 end
1079 1079
1080 def test_thumbnail_tag
1081 a = Attachment.find(3)
1082 assert_equal '<a href="/attachments/3/logo.gif" title="logo.gif"><img alt="3" src="/attachments/thumbnail/3" /></a>',
1083 thumbnail_tag(a)
1084 end
1085
1080 1086 def test_link_to_project
1081 1087 project = Project.find(1)
1082 1088 assert_equal %(<a href="/projects/ecookbook">eCookbook</a>),
1083 1089 link_to_project(project)
1084 1090 assert_equal %(<a href="/projects/ecookbook/settings">eCookbook</a>),
1085 1091 link_to_project(project, :action => 'settings')
1086 1092 assert_equal %(<a href="http://test.host/projects/ecookbook?jump=blah">eCookbook</a>),
1087 1093 link_to_project(project, {:only_path => false, :jump => 'blah'})
1088 1094 assert_equal %(<a href="/projects/ecookbook/settings" class="project">eCookbook</a>),
1089 1095 link_to_project(project, {:action => 'settings'}, :class => "project")
1090 1096 end
1091 1097
1092 1098 def test_link_to_project_settings
1093 1099 project = Project.find(1)
1094 1100 assert_equal '<a href="/projects/ecookbook/settings">eCookbook</a>', link_to_project_settings(project)
1095 1101
1096 1102 project.status = Project::STATUS_CLOSED
1097 1103 assert_equal '<a href="/projects/ecookbook">eCookbook</a>', link_to_project_settings(project)
1098 1104
1099 1105 project.status = Project::STATUS_ARCHIVED
1100 1106 assert_equal 'eCookbook', link_to_project_settings(project)
1101 1107 end
1102 1108
1103 1109 def test_link_to_legacy_project_with_numerical_identifier_should_use_id
1104 1110 # numeric identifier are no longer allowed
1105 1111 Project.update_all "identifier=25", "id=1"
1106 1112
1107 1113 assert_equal '<a href="/projects/1">eCookbook</a>',
1108 1114 link_to_project(Project.find(1))
1109 1115 end
1110 1116
1111 1117 def test_principals_options_for_select_with_users
1112 1118 User.current = nil
1113 1119 users = [User.find(2), User.find(4)]
1114 1120 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>),
1115 1121 principals_options_for_select(users)
1116 1122 end
1117 1123
1118 1124 def test_principals_options_for_select_with_selected
1119 1125 User.current = nil
1120 1126 users = [User.find(2), User.find(4)]
1121 1127 assert_equal %(<option value="2">John Smith</option><option value="4" selected="selected">Robert Hill</option>),
1122 1128 principals_options_for_select(users, User.find(4))
1123 1129 end
1124 1130
1125 1131 def test_principals_options_for_select_with_users_and_groups
1126 1132 User.current = nil
1127 1133 users = [User.find(2), Group.find(11), User.find(4), Group.find(10)]
1128 1134 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>) +
1129 1135 %(<optgroup label="Groups"><option value="10">A Team</option><option value="11">B Team</option></optgroup>),
1130 1136 principals_options_for_select(users)
1131 1137 end
1132 1138
1133 1139 def test_principals_options_for_select_with_empty_collection
1134 1140 assert_equal '', principals_options_for_select([])
1135 1141 end
1136 1142
1137 1143 def test_principals_options_for_select_should_include_me_option_when_current_user_is_in_collection
1138 1144 users = [User.find(2), User.find(4)]
1139 1145 User.current = User.find(4)
1140 1146 assert_include '<option value="4">&lt;&lt; me &gt;&gt;</option>', principals_options_for_select(users)
1141 1147 end
1142 1148
1143 1149 def test_stylesheet_link_tag_should_pick_the_default_stylesheet
1144 1150 assert_match 'href="/stylesheets/styles.css"', stylesheet_link_tag("styles")
1145 1151 end
1146 1152
1147 1153 def test_stylesheet_link_tag_for_plugin_should_pick_the_plugin_stylesheet
1148 1154 assert_match 'href="/plugin_assets/foo/stylesheets/styles.css"', stylesheet_link_tag("styles", :plugin => :foo)
1149 1155 end
1150 1156
1151 1157 def test_image_tag_should_pick_the_default_image
1152 1158 assert_match 'src="/images/image.png"', image_tag("image.png")
1153 1159 end
1154 1160
1155 1161 def test_image_tag_should_pick_the_theme_image_if_it_exists
1156 1162 theme = Redmine::Themes.themes.last
1157 1163 theme.images << 'image.png'
1158 1164
1159 1165 with_settings :ui_theme => theme.id do
1160 1166 assert_match %|src="/themes/#{theme.dir}/images/image.png"|, image_tag("image.png")
1161 1167 assert_match %|src="/images/other.png"|, image_tag("other.png")
1162 1168 end
1163 1169 ensure
1164 1170 theme.images.delete 'image.png'
1165 1171 end
1166 1172
1167 1173 def test_image_tag_sfor_plugin_should_pick_the_plugin_image
1168 1174 assert_match 'src="/plugin_assets/foo/images/image.png"', image_tag("image.png", :plugin => :foo)
1169 1175 end
1170 1176
1171 1177 def test_javascript_include_tag_should_pick_the_default_javascript
1172 1178 assert_match 'src="/javascripts/scripts.js"', javascript_include_tag("scripts")
1173 1179 end
1174 1180
1175 1181 def test_javascript_include_tag_for_plugin_should_pick_the_plugin_javascript
1176 1182 assert_match 'src="/plugin_assets/foo/javascripts/scripts.js"', javascript_include_tag("scripts", :plugin => :foo)
1177 1183 end
1178 1184 end
General Comments 0
You need to be logged in to leave comments. Login now