application_helper.rb 34.2 KB
Newer Older
1 2
# Redmine - project management software
# Copyright (C) 2006-2010  Jean-Philippe Lang
3 4 5 6 7
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
8
#
9 10 11 12
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
13
#
14 15 16 17
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

18
require 'forwardable'
19
require 'cgi'
20

21
module ApplicationHelper
22
  include Redmine::WikiFormatting::Macros::Definitions
23
  include Redmine::I18n
24
  include GravatarHelper::PublicMethods
25

26 27 28
  extend Forwardable
  def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter

29
  # Return true if user is authorized for controller/action, otherwise false
jplang's avatar
jplang committed
30 31
  def authorize_for(controller, action)
    User.current.allowed_to?({:controller => controller, :action => action}, @project)
32 33 34
  end

  # Display a link if user is authorized
35 36
  #
  # @param [String] name Anchor text (passed to link_to)
edavis10's avatar
edavis10 committed
37
  # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
38 39
  # @param [optional, Hash] html_options Options passed to link_to
  # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
40
  def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
edavis10's avatar
edavis10 committed
41
    link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
42
  end
43

44 45 46 47
  # Display a link to remote if user is authorized
  def link_to_remote_if_authorized(name, options = {}, html_options = nil)
    url = options[:url] || {}
    link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
48 49
  end

50
  # Displays a link to user's account page if active
51
  def link_to_user(user, options={})
jplang's avatar
jplang committed
52
    if user.is_a?(User)
53 54 55 56 57 58
      name = h(user.name(options[:format]))
      if user.active?
        link_to name, :controller => 'users', :action => 'show', :id => user
      else
        name
      end
jplang's avatar
jplang committed
59
    else
60
      h(user.to_s)
jplang's avatar
jplang committed
61
    end
62
  end
63

64 65 66 67 68 69
  # Displays a link to +issue+ with its subject.
  # Examples:
  # 
  #   link_to_issue(issue)                        # => Defect #6: This is the subject
  #   link_to_issue(issue, :truncate => 6)        # => Defect #6: This i...
  #   link_to_issue(issue, :subject => false)     # => Defect #6
70
  #   link_to_issue(issue, :project => true)      # => Foo - Defect #6
71
  #
72
  def link_to_issue(issue, options={})
73 74 75 76 77 78 79 80 81 82 83 84 85 86
    title = nil
    subject = nil
    if options[:subject] == false
      title = truncate(issue.subject, :length => 60)
    else
      subject = issue.subject
      if options[:truncate]
        subject = truncate(subject, :length => options[:truncate])
      end
    end
    s = link_to "#{issue.tracker} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue}, 
                                                 :class => issue.css_classes,
                                                 :title => title
    s << ": #{h subject}" if subject
87
    s = "#{h issue.project} - " + s if options[:project]
88
    s
jplang's avatar
jplang committed
89
  end
90

91 92 93 94 95 96 97
  # Generates a link to an attachment.
  # Options:
  # * :text - Link text (default to attachment filename)
  # * :download - Force download (default: false)
  def link_to_attachment(attachment, options={})
    text = options.delete(:text) || attachment.filename
    action = options.delete(:download) ? 'download' : 'show'
98

99 100
    link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
  end
101

102 103 104 105 106 107 108 109 110
  # Generates a link to a SCM revision
  # Options:
  # * :text - Link text (default to the formatted revision)
  def link_to_revision(revision, project, options={})
    text = options.delete(:text) || format_revision(revision)

    link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => revision}, :title => l(:label_revision_id, revision))
  end

111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127
  # Generates a link to a project if active
  # Examples:
  # 
  #   link_to_project(project)                          # => link to the specified project overview
  #   link_to_project(project, :action=>'settings')     # => link to project settings
  #   link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
  #   link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
  #
  def link_to_project(project, options={}, html_options = nil)
    if project.active?
      url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
      link_to(h(project), url, html_options)
    else
      h(project)
    end
  end

jplang's avatar
jplang committed
128 129 130 131 132 133
  def toggle_link(name, id, options={})
    onclick = "Element.toggle('#{id}'); "
    onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
    onclick << "return false;"
    link_to(name, "#", :onclick => onclick)
  end
134

135 136
  def image_to_function(name, function, html_options = {})
    html_options.symbolize_keys!
137 138 139
    tag(:input, html_options.merge({
        :type => "image", :src => image_path(name),
        :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
140 141
        }))
  end
142

143 144 145 146
  def prompt_to_remote(name, text, param, url, html_options = {})
    html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
    link_to name, {}, html_options
  end
147 148
  
  def format_activity_title(text)
149
    h(truncate_single_line(text, :length => 100))
150 151 152 153 154 155 156
  end
  
  def format_activity_day(date)
    date == Date.today ? l(:label_today).titleize : format_date(date)
  end
  
  def format_activity_description(text)
157
    h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
158
  end
159

160 161 162 163 164 165 166 167
  def format_version_name(version)
    if version.project == @project
    	h(version)
    else
      h("#{version.project} - #{version}")
    end
  end
  
168 169 170 171 172
  def due_date_distance_in_words(date)
    if date
      l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
    end
  end
173

174 175 176 177 178 179
  def render_page_hierarchy(pages, node=nil)
    content = ''
    if pages[node]
      content << "<ul class=\"pages-hierarchy\">\n"
      pages[node].each do |page|
        content << "<li>"
180
        content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
181 182 183 184 185 186 187 188
                           :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
        content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
        content << "</li>\n"
      end
      content << "</ul>\n"
    end
    content
  end
189 190 191 192 193 194 195 196 197
  
  # Renders flash messages
  def render_flash_messages
    s = ''
    flash.each do |k,v|
      s << content_tag('div', v, :class => "flash #{k}")
    end
    s
  end
198
  
jplang's avatar
jplang committed
199 200 201 202 203 204 205 206 207
  # Renders tabs and their content
  def render_tabs(tabs)
    if tabs.any?
      render :partial => 'common/tabs', :locals => {:tabs => tabs}
    else
      content_tag 'p', l(:label_no_data), :class => "nodata"
    end
  end
  
208 209 210 211 212 213
  # Renders the project quick-jump box
  def render_project_jump_box
    # Retrieve them now to avoid a COUNT query
    projects = User.current.projects.all
    if projects.any?
      s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
214
            "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
jplang's avatar
jplang committed
215
            '<option value="" disabled="disabled">---</option>'
216
      s << project_tree_options_for_select(projects, :selected => @project) do |p|
217
        { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
218 219 220 221 222 223 224 225 226 227
      end
      s << '</select>'
      s
    end
  end
  
  def project_tree_options_for_select(projects, options = {})
    s = ''
    project_tree(projects) do |project, level|
      name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
228 229 230 231 232 233
      tag_options = {:value => project.id}
      if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
        tag_options[:selected] = 'selected'
      else
        tag_options[:selected] = nil
      end
234 235 236 237 238 239 240
      tag_options.merge!(yield(project)) if block_given?
      s << content_tag('option', name_prefix + h(project), tag_options)
    end
    s
  end
  
  # Yields the given block for each project with its level in the tree
241 242
  #
  # Wrapper for Project#project_tree
243
  def project_tree(projects, &block)
244
    Project.project_tree(projects, &block)
245
  end
246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269
  
  def project_nested_ul(projects, &block)
    s = ''
    if projects.any?
      ancestors = []
      projects.sort_by(&:lft).each do |project|
        if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
          s << "<ul>\n"
        else
          ancestors.pop
          s << "</li>"
          while (ancestors.any? && !project.is_descendant_of?(ancestors.last)) 
            ancestors.pop
            s << "</ul></li>\n"
          end
        end
        s << "<li>"
        s << yield(project).to_s
        ancestors << project
      end
      s << ("</li></ul>\n" * ancestors.size)
    end
    s
  end
jplang's avatar
jplang committed
270 271 272
  
  def principals_check_box_tags(name, principals)
    s = ''
273
    principals.sort.each do |principal|
jplang's avatar
jplang committed
274 275 276 277
      s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
    end
    s 
  end
278

279 280
  # Truncates and returns the string as a single line
  def truncate_single_line(string, *args)
281
    truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
282
  end
283 284 285 286 287 288 289 290 291 292
  
  # Truncates at line break after 250 characters or options[:length]
  def truncate_lines(string, options={})
    length = options[:length] || 250
    if string.to_s =~ /\A(.{#{length}}.*?)$/m
      "#{$1}..."
    else
      string
    end
  end
293

294 295 296
  def html_hours(text)
    text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
  end
297

jplang's avatar
jplang committed
298
  def authoring(created, author, options={})
299
    l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created))
300 301 302 303 304
  end
  
  def time_tag(time)
    text = distance_of_time_in_words(Time.now, time)
    if @project
305
      link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
306 307 308
    else
      content_tag('acronym', text, :title => format_time(time))
    end
309 310
  end

311
  def syntax_highlight(name, content)
312
    Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
313
  end
314

jplang's avatar
jplang committed
315 316 317 318
  def to_path_param(path)
    path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
  end

319
  def pagination_links_full(paginator, count=nil, options={})
320
    page_param = options.delete(:page_param) || :page
321
    per_page_links = options.delete(:per_page_links)
322
    url_param = params.dup
323 324
    # don't reuse query params if filters are present
    url_param.merge!(:fields => nil, :values => nil, :operators => nil) if url_param.delete(:set_filter)
325 326

    html = ''
327 328 329
    if paginator.current.previous
      html << link_to_remote_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
    end
330

331
    html << (pagination_links_each(paginator, options) do |n|
332
      link_to_remote_content_update(n.to_s, url_param.merge(page_param => n))
333
    end || '')
334 335 336 337
    
    if paginator.current.next
      html << ' ' + link_to_remote_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next))
    end
338

339
    unless count.nil?
340 341 342 343
      html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
      if per_page_links != false && links = per_page_links(paginator.items_per_page)
	      html << " | #{links}"
      end
344
    end
345 346

    html
347
  end
348
  
349
  def per_page_links(selected=nil)
350 351
    url_param = params.dup
    url_param.clear if url_param.has_key?(:set_filter)
352

353
    links = Setting.per_page_options_array.collect do |n|
354 355 356
      n == selected ? n : link_to_remote(n, {:update => "content",
                                             :url => params.dup.merge(:per_page => n),
                                             :method => :get},
357
                                            {:href => url_for(url_param.merge(:per_page => n))})
358 359 360
    end
    links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
  end
jplang's avatar
jplang committed
361 362 363 364 365 366 367
  
  def reorder_links(name, url)
    link_to(image_tag('2uparrow.png',   :alt => l(:label_sort_highest)), url.merge({"#{name}[move_to]" => 'highest'}), :method => :post, :title => l(:label_sort_highest)) +
    link_to(image_tag('1uparrow.png',   :alt => l(:label_sort_higher)),  url.merge({"#{name}[move_to]" => 'higher'}),  :method => :post, :title => l(:label_sort_higher)) +
    link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),   url.merge({"#{name}[move_to]" => 'lower'}),   :method => :post, :title => l(:label_sort_lower)) +
    link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),  url.merge({"#{name}[move_to]" => 'lowest'}),  :method => :post, :title => l(:label_sort_lowest))
  end
368

369
  def breadcrumb(*args)
370 371
    elements = args.flatten
    elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
372
  end
373 374
  
  def other_formats_links(&block)
375
    concat('<p class="other-formats">' + l(:label_export_to))
376
    yield Redmine::Views::OtherFormatsBuilder.new(self)
377
    concat('</p>')
378
  end
379 380 381 382 383 384 385 386 387
  
  def page_header_title
    if @project.nil? || @project.new_record?
      h(Setting.app_title)
    else
      b = []
      ancestors = (@project.root? ? [] : @project.ancestors.visible)
      if ancestors.any?
        root = ancestors.shift
388
        b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
389 390 391 392
        if ancestors.size > 2
          b << '&#8230;'
          ancestors = ancestors[-2, 2]
        end
393
        b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
394 395 396 397 398
      end
      b << h(@project)
      b.join(' &#187; ')
    end
  end
399

400 401 402 403 404 405
  def html_title(*args)
    if args.empty?
      title = []
      title << @project.name if @project
      title += @html_title if @html_title
      title << Setting.app_title
jplang's avatar
jplang committed
406
      title.select {|t| !t.blank? }.join(' - ')
407 408 409 410
    else
      @html_title ||= []
      @html_title += args
    end
411
  end
jplang's avatar
jplang committed
412

413 414 415 416 417 418 419 420 421 422 423 424 425
  # Returns the theme, controller name, and action as css classes for the
  # HTML body.
  def body_css_classes
    css = []
    if theme = Redmine::Themes.theme(Setting.ui_theme)
      css << 'theme-' + theme.name
    end

    css << 'controller-' + params[:controller]
    css << 'action-' + params[:action]
    css.join(' ')
  end

jplang's avatar
jplang committed
426
  def accesskey(s)
427
    Redmine::AccessKeys.key_for s
jplang's avatar
jplang committed
428 429
  end

430 431 432 433 434 435 436 437
  # Formats text according to system settings.
  # 2 ways to call this method:
  # * with a String: textilizable(text, options)
  # * with an object and one of its attribute: textilizable(issue, :description, options)
  def textilizable(*args)
    options = args.last.is_a?(Hash) ? args.pop : {}
    case args.size
    when 1
438
      obj = options[:object]
jplang's avatar
jplang committed
439
      text = args.shift
440 441
    when 2
      obj = args.shift
442 443
      attr = args.shift
      text = obj.send(attr).to_s
444 445 446
    else
      raise ArgumentError, 'invalid arguments to textilizable'
    end
jplang's avatar
jplang committed
447
    return '' if text.blank?
448 449
    project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
    only_path = options.delete(:only_path) == false ? false : true
450

451
    text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }
452 453 454
    
    @parsed_headings = []
    text = parse_non_pre_blocks(text) do |text|
455
      [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_headings].each do |method_name|
456 457
        send method_name, text, project, obj, attr, only_path, options
      end
458
    end
459 460 461 462 463 464
    
    if @parsed_headings.any?
      replace_toc(text, @parsed_headings)
    end
    
    text
465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488
  end
  
  def parse_non_pre_blocks(text)
    s = StringScanner.new(text)
    tags = []
    parsed = ''
    while !s.eos?
      s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
      text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
      if tags.empty?
        yield text
      end
      parsed << text
      if tag
        if closing
          if tags.last == tag.downcase
            tags.pop
          end
        else
          tags << tag.downcase
        end
        parsed << full_tag
      end
    end
489 490 491 492
    # Close any non closing tags
    while tag = tags.pop
      parsed << "</#{tag}>"
    end
493
    parsed
494 495 496
  end
  
  def parse_inline_attachments(text, project, obj, attr, only_path, options)
497
    # when using an image link, try to use an attachment, if possible
498 499
    if options[:attachments] || (obj && obj.respond_to?(:attachments))
      attachments = nil
500 501
      text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
        filename, ext, alt, alttext = $1.downcase, $2, $3, $4 
502
        attachments ||= (options[:attachments] || obj.attachments).sort_by(&:created_on).reverse
503
        # search for the picture in attachments
504
        if found = attachments.detect { |att| att.filename.downcase == filename }
505
          image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
506 507 508 509 510
          desc = found.description.to_s.gsub('"', '')
          if !desc.blank? && alttext.blank?
            alt = " title=\"#{desc}\" alt=\"#{desc}\""
          end
          "src=\"#{image_url}\"#{alt}"
511
        else
512
          m
513 514 515
        end
      end
    end
516
  end
517

518 519 520 521 522 523 524 525 526 527 528 529
  # Wiki links
  #
  # Examples:
  #   [[mypage]]
  #   [[mypage|mytext]]
  # wiki links can refer other project wikis, using project name or identifier:
  #   [[project:]] -> wiki starting page
  #   [[project:|mytext]]
  #   [[project:mypage]]
  #   [[project:mypage|mytext]]
  def parse_wiki_links(text, project, obj, attr, only_path, options)
    text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
530
      link_project = project
531 532 533
      esc, all, page, title = $1, $2, $3, $5
      if esc.nil?
        if page =~ /^([^\:]+)\:(.*)$/
534
          link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
535 536 537
          page = $2
          title ||= $1 if page.blank?
        end
538

539
        if link_project && link_project.wiki
540 541 542 543 544
          # extract anchor
          anchor = nil
          if page =~ /^(.+?)\#(.+)$/
            page, anchor = $1, $2
          end
545 546
          # check if page exists
          wiki_page = link_project.wiki.find_page(page)
547 548 549 550
          url = case options[:wiki_links]
            when :local; "#{title}.html"
            when :anchor; "##{title}"   # used for single-file wiki export
            else
551 552
              wiki_page_id = page.present? ? Wiki.titleize(page) : nil
              url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, :id => wiki_page_id, :anchor => anchor)
553 554
            end
          link_to((title || page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
555 556
        else
          # project or wiki doesn't exist
557
          all
558
        end
jplang's avatar
jplang committed
559
      else
560
        all
jplang's avatar
jplang committed
561
      end
562
    end
563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591
  end
  
  # Redmine links
  #
  # Examples:
  #   Issues:
  #     #52 -> Link to issue #52
  #   Changesets:
  #     r52 -> Link to revision 52
  #     commit:a85130f -> Link to scmid starting with a85130f
  #   Documents:
  #     document#17 -> Link to document with id 17
  #     document:Greetings -> Link to the document with title "Greetings"
  #     document:"Some document" -> Link to the document with title "Some document"
  #   Versions:
  #     version#3 -> Link to version with id 3
  #     version:1.0.0 -> Link to version named "1.0.0"
  #     version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
  #   Attachments:
  #     attachment:file.zip -> Link to the attachment of the current object named file.zip
  #   Source files:
  #     source:some/file -> Link to the file located at /some/file in the project's repository
  #     source:some/file@52 -> Link to the file's revision 52
  #     source:some/file#L120 -> Link to line 120 of the file
  #     source:some/file@52#L120 -> Link to line 120 of the file's revision 52
  #     export:some/file -> Force the download of the file
  #  Forum messages:
  #     message#1218 -> Link to message with id 1218
  def parse_redmine_links(text, project, obj, attr, only_path, options)
592
    text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(attachment|document|version|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m|
593
      leading, esc, prefix, sep, identifier = $1, $2, $3, $5 || $7, $6 || $8
594
      link = nil
595 596
      if esc.nil?
        if prefix.nil? && sep == 'r'
597 598
          if project && (changeset = project.changesets.find_by_revision(identifier))
            link = link_to("r#{identifier}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
jplang's avatar
jplang committed
599
                                      :class => 'changeset',
600
                                      :title => truncate_single_line(changeset.comments, :length => 100))
601 602
          end
        elsif sep == '#'
603
          oid = identifier.to_i
604 605
          case prefix
          when nil
jplang's avatar
jplang committed
606
            if issue = Issue.visible.find_by_id(oid, :include => :status)
jplang's avatar
jplang committed
607
              link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
jplang's avatar
jplang committed
608
                                        :class => issue.css_classes,
609
                                        :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
610 611 612
            end
          when 'document'
            if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
jplang's avatar
jplang committed
613 614
              link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
                                                :class => 'document'
615 616 617
            end
          when 'version'
            if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
jplang's avatar
jplang committed
618 619
              link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
                                              :class => 'version'
620
            end
621 622
          when 'message'
            if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
623
              link = link_to h(truncate(message.subject, :length => 60)), {:only_path => only_path,
624 625 626 627 628 629 630
                                                                :controller => 'messages',
                                                                :action => 'show',
                                                                :board_id => message.board,
                                                                :id => message.root,
                                                                :anchor => (message.parent ? "message-#{message.id}" : nil)},
                                                 :class => 'message'
            end
jplang's avatar
jplang committed
631 632
          when 'project'
            if p = Project.visible.find_by_id(oid)
633
              link = link_to_project(p, {:only_path => only_path}, :class => 'project')
jplang's avatar
jplang committed
634
            end
635 636 637
          end
        elsif sep == ':'
          # removes the double quotes if any
638
          name = identifier.gsub(%r{^"(.*)"$}, "\\1")
639 640 641
          case prefix
          when 'document'
            if project && document = project.documents.find_by_title(name)
jplang's avatar
jplang committed
642 643
              link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
                                                :class => 'document'
644 645 646
            end
          when 'version'
            if project && version = project.versions.find_by_name(name)
jplang's avatar
jplang committed
647 648
              link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
                                              :class => 'version'
649
            end
650 651
          when 'commit'
            if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
652 653
              link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
                                           :class => 'changeset',
654
                                           :title => truncate_single_line(changeset.comments, :length => 100)
655 656 657 658 659
            end
          when 'source', 'export'
            if project && project.repository
              name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
              path, rev, anchor = $1, $3, $5
jplang's avatar
jplang committed
660 661
              link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
                                                      :path => to_path_param(path),
662 663 664 665
                                                      :rev => rev,
                                                      :anchor => anchor,
                                                      :format => (prefix == 'export' ? 'raw' : nil)},
                                                     :class => (prefix == 'export' ? 'source download' : 'source')
666
            end
667
          when 'attachment'
668
            attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
669
            if attachments && attachment = attachments.detect {|a| a.filename == name }
jplang's avatar
jplang committed
670 671
              link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
                                                     :class => 'attachment'
672
            end
jplang's avatar
jplang committed
673 674
          when 'project'
            if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
675
              link = link_to_project(p, {:only_path => only_path}, :class => 'project')
jplang's avatar
jplang committed
676
            end
677
          end
jplang's avatar
jplang committed
678
        end
679
      end
680
      leading + (link || "#{prefix}#{sep}#{identifier}")
681
    end
682
  end
683
  
684
  HEADING_RE = /<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>/i unless const_defined?(:HEADING_RE)
685 686
  
  # Headings and TOC
687
  # Adds ids and links to headings unless options[:headings] is set to false
688
  def parse_headings(text, project, obj, attr, only_path, options)
689 690
    return if options[:headings] == false
    
691
    text.gsub!(HEADING_RE) do
jplang's avatar
jplang committed
692
      level, attrs, content = $1.to_i, $2, $3
693 694
      item = strip_tags(content).strip
      anchor = item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
695
      @parsed_headings << [level, anchor, item]
696
      "<h#{level} #{attrs} id=\"#{anchor}\">#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
697 698 699 700 701 702 703
    end
  end
          
  TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
  
  # Renders the TOC with given headings
  def replace_toc(text, headings)
704 705 706 707 708 709 710
    text.gsub!(TOC_RE) do
      if headings.empty?
        ''
      else
        div_class = 'toc'
        div_class << ' right' if $1 == '>'
        div_class << ' left' if $1 == '<'
jplang's avatar
jplang committed
711 712 713 714
        out = "<ul class=\"#{div_class}\"><li>"
        root = headings.map(&:first).min
        current = root
        started = false
715
        headings.each do |level, anchor, item|
jplang's avatar
jplang committed
716 717 718 719 720 721 722 723 724 725
          if level > current
            out << '<ul><li>' * (level - current)
          elsif level < current
            out << "</li></ul>\n" * (current - level) + "</li><li>"
          elsif started
            out << '</li><li>'
          end
          out << "<a href=\"##{anchor}\">#{item}</a>"
          current = level
          started = true
726
        end
jplang's avatar
jplang committed
727 728
        out << '</li></ul>' * (current - root)
        out << '</li></ul>'
729 730 731
      end
    end
  end
732

jplang's avatar
jplang committed
733 734 735 736 737 738 739
  # Same as Rails' simple_format helper without using paragraphs
  def simple_format_without_paragraph(text)
    text.to_s.
      gsub(/\r\n?/, "\n").                    # \r\n and \r -> \n
      gsub(/\n\n+/, "<br /><br />").          # 2+ newline  -> 2 br
      gsub(/([^\n]\n)(?=[^\n])/, '\1<br />')  # 1 newline   -> br
  end
740

741
  def lang_options_for_select(blank=true)
742
    (blank ? [["(auto)", ""]] : []) +
743
      valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
744
  end
745

746 747 748 749
  def label_tag_for(name, option_tags = nil, options = {})
    label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
    content_tag("label", label_text)
  end
750

751 752
  def labelled_tabular_form_for(name, object, options, &proc)
    options[:html] ||= {}
753
    options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
754 755
    form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
  end
756

757
  def back_url_hidden_field_tag
758
    back_url = params[:back_url] || request.env['HTTP_REFERER']
759
    back_url = CGI.unescape(back_url.to_s)
760
    hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
761
  end
762

763 764 765
  def check_all_links(form_name)
    link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
    " | " +
766
    link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
767
  end
768

769 770
  def progress_bar(pcts, options={})
    pcts = [pcts, pcts] unless pcts.is_a?(Array)
771
    pcts = pcts.collect(&:round)
772 773
    pcts[1] = pcts[1] - pcts[0]
    pcts << (100 - pcts[1] - pcts[0])
774 775 776 777
    width = options[:width] || '100px;'
    legend = options[:legend] || ''
    content_tag('table',
      content_tag('tr',
778 779 780
        (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : '') +
        (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : '') +
        (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : '')
781 782 783
      ), :class => 'progress', :style => "width: #{width};") +
      content_tag('p', legend, :class => 'pourcent')
  end
784
  
785 786 787 788 789 790
  def checked_image(checked=true)
    if checked
      image_tag 'toggle_check.png'
    end
  end
  
791 792 793 794 795 796
  def context_menu(url)
    unless @context_menu_included
      content_for :header_tags do
        javascript_include_tag('context_menu') +
          stylesheet_link_tag('context_menu')
      end
797 798 799 800 801
      if l(:direction) == 'rtl'
        content_for :header_tags do
          stylesheet_link_tag('context_menu_rtl')
        end
      end
802 803 804 805
      @context_menu_included = true
    end
    javascript_tag "new ContextMenu('#{ url_for(url) }')"
  end
806

807 808 809 810 811 812 813 814 815 816 817 818 819 820 821
  def context_menu_link(name, url, options={})
    options[:class] ||= ''
    if options.delete(:selected)
      options[:class] << ' icon-checked disabled'
      options[:disabled] = true
    end
    if options.delete(:disabled)
      options.delete(:method)
      options.delete(:confirm)
      options.delete(:onclick)
      options[:class] << ' disabled'
      url = '#'
    end
    link_to name, url, options
  end
822

823
  def calendar_for(field_id)
824
    include_calendar_headers_tags
825 826 827
    image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
    javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
  end
828 829 830 831 832

  def include_calendar_headers_tags
    unless @calendar_headers_tags_included
      @calendar_headers_tags_included = true
      content_for :header_tags do
833 834 835 836 837 838 839 840 841
        start_of_week = case Setting.start_of_week.to_i
        when 1
          'Calendar._FD = 1;' # Monday
        when 7
          'Calendar._FD = 0;' # Sunday
        else
          '' # use language
        end
        
842
        javascript_include_tag('calendar/calendar') +
843
        javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
844
        javascript_tag(start_of_week) +  
845 846 847 848 849
        javascript_include_tag('calendar/calendar-setup') +
        stylesheet_link_tag('calendar')
      end
    end
  end
850

jplang's avatar
jplang committed
851 852 853 854 855
  def content_for(name, content = nil, &block)
    @has_content ||= {}
    @has_content[name] = true
    super(name, content, &block)
  end
856

jplang's avatar
jplang committed
857 858 859
  def has_content?(name)
    (@has_content && @has_content[name]) || false
  end
860

861 862 863
  # Returns the avatar image tag for the given +user+ if avatars are enabled
  # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
  def avatar(user, options = { })
864
    if Setting.gravatar_enabled?
865
      options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
866 867 868 869 870 871 872
      email = nil
      if user.respond_to?(:mail)
        email = user.mail
      elsif user.to_s =~ %r{<(.+?)>}
        email = $1
      end
      return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
edavis10's avatar
edavis10 committed
873 874
    else
      ''
875 876 877
    end
  end

878
  def favicon
879
    "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />"
880
  end
881 882 883 884 885 886 887 888 889 890
  
  # Returns true if arg is expected in the API response
  def include_in_api_response?(arg)
    unless @included_in_api_response
      param = params[:include]
      @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
      @included_in_api_response.collect!(&:strip)
    end
    @included_in_api_response.include?(arg.to_s)
  end
891

892 893 894 895 896 897 898 899 900 901 902 903
  # Returns options or nil if nometa param or X-Redmine-Nometa header
  # was set in the request
  def api_meta(options)
    if params[:nometa].present? || request.headers['X-Redmine-Nometa']
      # compatibility mode for activeresource clients that raise
      # an error when unserializing an array with attributes
      nil
    else
      options
    end
  end
  
904
  private
905

906 907 908 909 910
  def wiki_helper
    helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
    extend helper
    return self
  end
911 912 913 914 915 916 917 918
  
  def link_to_remote_content_update(text, url_params)
    link_to_remote(text,
      {:url => url_params, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'},
      {:href => url_for(:params => url_params)}
    )
  end
  
jplang's avatar
jplang committed
919
end