application_helper.rb 36.3 KB
Newer Older
1
# Redmine - project management software
2
# Copyright (C) 2006-2011  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
  # Displays a link to +issue+ with its subject.
  # Examples:
66
  #
67 68 69
  #   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
    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
tmaruyama's avatar
tmaruyama committed
83
    s = link_to "#{h(issue.tracker)} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
84 85 86
                                                 :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
  # 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)
107
    rev = revision.respond_to?(:identifier) ? revision.identifier : revision
108

109
    link_to(h(text), {:controller => 'repositories', :action => 'revision', :id => project, :rev => rev},
110
            :title => l(:label_revision_id, format_revision(revision)))
111
  end
112

113 114 115 116 117 118 119 120 121 122 123 124 125
  # Generates a link to a message
  def link_to_message(message, options={}, html_options = nil)
    link_to(
      h(truncate(message.subject, :length => 60)),
      { :controller => 'messages', :action => 'show',
        :board_id => message.board_id,
        :id => message.root,
        :r => (message.parent_id && message.id),
        :anchor => (message.parent_id ? "message-#{message.id}" : nil)
      }.merge(options),
      html_options
    )
  end
126

127 128
  # Generates a link to a project if active
  # Examples:
129
  #
130 131 132 133 134 135 136 137 138 139 140 141 142 143
  #   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
144 145 146 147 148 149
  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
150

151 152
  def image_to_function(name, function, html_options = {})
    html_options.symbolize_keys!
153 154 155
    tag(:input, html_options.merge({
        :type => "image", :src => image_path(name),
        :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
156 157
        }))
  end
158

159 160 161 162
  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
163

164
  def format_activity_title(text)
165
    h(truncate_single_line(text, :length => 100))
166
  end
167

168 169 170
  def format_activity_day(date)
    date == Date.today ? l(:label_today).titleize : format_date(date)
  end
171

172
  def format_activity_description(text)
173
    h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
174
  end
175

176 177 178 179 180 181 182
  def format_version_name(version)
    if version.project == @project
    	h(version)
    else
      h("#{version.project} - #{version}")
    end
  end
183

184 185 186 187 188
  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
189

190
  def render_page_hierarchy(pages, node=nil, options={})
191 192 193 194 195
    content = ''
    if pages[node]
      content << "<ul class=\"pages-hierarchy\">\n"
      pages[node].each do |page|
        content << "<li>"
196
        content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
197 198
                           :title => (options[:timestamp] && page.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, options) if pages[page.id]
199 200 201 202 203 204
        content << "</li>\n"
      end
      content << "</ul>\n"
    end
    content
  end
205

206 207 208 209 210 211
  # Renders flash messages
  def render_flash_messages
    s = ''
    flash.each do |k,v|
      s << content_tag('div', v, :class => "flash #{k}")
    end
212
    s.html_safe
213
  end
214

jplang's avatar
jplang committed
215 216 217 218 219 220 221 222
  # 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
223

224 225
  # Renders the project quick-jump box
  def render_project_jump_box
226
    return unless User.current.logged?
227
    projects = User.current.memberships.collect(&:project).compact.uniq
228 229
    if projects.any?
      s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
230
            "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
jplang's avatar
jplang committed
231
            '<option value="" disabled="disabled">---</option>'
232
      s << project_tree_options_for_select(projects, :selected => @project) do |p|
233
        { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
234 235
      end
      s << '</select>'
236
      s.html_safe
237 238
    end
  end
239

240 241 242 243
  def project_tree_options_for_select(projects, options = {})
    s = ''
    project_tree(projects) do |project, level|
      name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
244 245 246 247 248 249
      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
250 251 252
      tag_options.merge!(yield(project)) if block_given?
      s << content_tag('option', name_prefix + h(project), tag_options)
    end
253
    s.html_safe
254
  end
255

256
  # Yields the given block for each project with its level in the tree
257 258
  #
  # Wrapper for Project#project_tree
259
  def project_tree(projects, &block)
260
    Project.project_tree(projects, &block)
261
  end
262

263 264 265 266 267 268 269 270 271 272
  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>"
273
          while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
274 275 276 277 278 279 280 281 282 283
            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
284
    s.html_safe
285
  end
286

jplang's avatar
jplang committed
287 288
  def principals_check_box_tags(name, principals)
    s = ''
289
    principals.sort.each do |principal|
jplang's avatar
jplang committed
290 291
      s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
    end
292
    s.html_safe
jplang's avatar
jplang committed
293
  end
294

295 296 297 298 299 300 301 302 303 304 305 306 307
  # Returns a string for users/groups option tags
  def principals_options_for_select(collection, selected=nil)
    s = ''
    groups = ''
    collection.sort.each do |element|
      selected_attribute = ' selected="selected"' if option_value_selected?(element, selected)
      (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
    end
    unless groups.empty?
      s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
    end
    s
  end
308

309 310
  # Truncates and returns the string as a single line
  def truncate_single_line(string, *args)
311
    truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
312
  end
313

314 315 316 317 318 319 320 321 322
  # 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
323

324
  def html_hours(text)
325
    text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
326
  end
327

jplang's avatar
jplang committed
328
  def authoring(created, author, options={})
329
    l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
330
  end
331

332 333 334
  def time_tag(time)
    text = distance_of_time_in_words(Time.now, time)
    if @project
335
      link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
336 337 338
    else
      content_tag('acronym', text, :title => format_time(time))
    end
339 340
  end

341
  def syntax_highlight(name, content)
342
    Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
343
  end
344

jplang's avatar
jplang committed
345 346 347 348
  def to_path_param(path)
    path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
  end

349
  def pagination_links_full(paginator, count=nil, options={})
350
    page_param = options.delete(:page_param) || :page
351
    per_page_links = options.delete(:per_page_links)
352
    url_param = params.dup
353 354

    html = ''
355
    if paginator.current.previous
356 357 358 359
      # \xc2\xab(utf-8) = &#171;
      html << link_to_content_update(
                   "\xc2\xab " + l(:label_previous),
                   url_param.merge(page_param => paginator.current.previous)) + ' '
360
    end
361

362
    html << (pagination_links_each(paginator, options) do |n|
363
      link_to_content_update(n.to_s, url_param.merge(page_param => n))
364
    end || '')
365

366
    if paginator.current.next
367 368 369 370
      # \xc2\xbb(utf-8) = &#187;
      html << ' ' + link_to_content_update(
                      (l(:label_next) + " \xc2\xbb"),
                      url_param.merge(page_param => paginator.current.next))
371
    end
372

373
    unless count.nil?
374 375 376 377
      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
378
    end
379

380
    html.html_safe
381
  end
382

383 384
  def per_page_links(selected=nil)
    links = Setting.per_page_options_array.collect do |n|
385
      n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
386 387 388
    end
    links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
  end
389

jplang's avatar
jplang committed
390 391 392 393 394 395
  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
396

397
  def breadcrumb(*args)
398
    elements = args.flatten
399
    elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
400
  end
401

402
  def other_formats_links(&block)
403
    concat('<p class="other-formats">' + l(:label_export_to))
404
    yield Redmine::Views::OtherFormatsBuilder.new(self)
405
    concat('</p>')
406
  end
407

408 409 410 411 412
  def page_header_title
    if @project.nil? || @project.new_record?
      h(Setting.app_title)
    else
      b = []
jplang's avatar
jplang committed
413
      ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
414 415
      if ancestors.any?
        root = ancestors.shift
416
        b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
417 418 419 420
        if ancestors.size > 2
          b << '&#8230;'
          ancestors = ancestors[-2, 2]
        end
421
        b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
422 423 424 425 426
      end
      b << h(@project)
      b.join(' &#187; ')
    end
  end
427

428 429 430
  def html_title(*args)
    if args.empty?
      title = []
tmaruyama's avatar
tmaruyama committed
431
      title << @project.name if @project
432 433
      title += @html_title if @html_title
      title << Setting.app_title
jplang's avatar
jplang committed
434
      title.select {|t| !t.blank? }.join(' - ')
435 436 437 438
    else
      @html_title ||= []
      @html_title += args
    end
439
  end
jplang's avatar
jplang committed
440

441 442 443 444 445 446 447 448 449 450 451 452 453
  # 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
454
  def accesskey(s)
455
    Redmine::AccessKeys.key_for s
jplang's avatar
jplang committed
456 457
  end

458 459 460 461 462 463 464 465
  # 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
466
      obj = options[:object]
jplang's avatar
jplang committed
467
      text = args.shift
468 469
    when 2
      obj = args.shift
470 471
      attr = args.shift
      text = obj.send(attr).to_s
472 473 474
    else
      raise ArgumentError, 'invalid arguments to textilizable'
    end
jplang's avatar
jplang committed
475
    return '' if text.blank?
476 477
    project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
    only_path = options.delete(:only_path) == false ? false : true
478

479
    text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }
480

481 482
    @parsed_headings = []
    text = parse_non_pre_blocks(text) do |text|
483
      [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_headings].each do |method_name|
484 485
        send method_name, text, project, obj, attr, only_path, options
      end
486
    end
487

488 489 490
    if @parsed_headings.any?
      replace_toc(text, @parsed_headings)
    end
491

492
    text
493
  end
494

495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516
  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
517 518 519 520
    # Close any non closing tags
    while tag = tags.pop
      parsed << "</#{tag}>"
    end
521
    parsed.html_safe
522
  end
523

524
  def parse_inline_attachments(text, project, obj, attr, only_path, options)
525
    # when using an image link, try to use an attachment, if possible
526 527
    if options[:attachments] || (obj && obj.respond_to?(:attachments))
      attachments = nil
528
      text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
529
        filename, ext, alt, alttext = $1.downcase, $2, $3, $4
530
        attachments ||= (options[:attachments] || obj.attachments).sort_by(&:created_on).reverse
531
        # search for the picture in attachments
532
        if found = attachments.detect { |att| att.filename.downcase == filename }
533
          image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
534 535 536 537
          desc = found.description.to_s.gsub('"', '')
          if !desc.blank? && alttext.blank?
            alt = " title=\"#{desc}\" alt=\"#{desc}\""
          end
538
          "src=\"#{image_url}\"#{alt}".html_safe
539
        else
540
          m.html_safe
541 542 543
        end
      end
    end
544
  end
545

546 547 548 549 550 551 552 553 554 555 556 557
  # 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|
558
      link_project = project
559 560 561
      esc, all, page, title = $1, $2, $3, $5
      if esc.nil?
        if page =~ /^([^\:]+)\:(.*)$/
562
          link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
563 564 565
          page = $2
          title ||= $1 if page.blank?
        end
566

567
        if link_project && link_project.wiki
568 569 570 571 572
          # extract anchor
          anchor = nil
          if page =~ /^(.+?)\#(.+)$/
            page, anchor = $1, $2
          end
573
          anchor = sanitize_anchor_name(anchor) if anchor.present?
574 575
          # check if page exists
          wiki_page = link_project.wiki.find_page(page)
576 577 578 579
          url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
            "##{anchor}"
          else
            case options[:wiki_links]
580
            when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
581
            when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
582
            else
583 584
              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)
585
            end
586
          end
587
          link_to(h(title || page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
588 589
        else
          # project or wiki doesn't exist
590
          all.html_safe
591
        end
jplang's avatar
jplang committed
592
      else
593
        all.html_safe
jplang's avatar
jplang committed
594
      end
595
    end
596
  end
597

598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621
  # 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
622
  #   Forum messages:
623
  #     message#1218 -> Link to message with id 1218
624 625 626 627 628 629
  #
  #   Links can refer other objects from other projects, using project identifier:
  #     identifier:r52
  #     identifier:document:"Some document"
  #     identifier:version:1.0.0
  #     identifier:source:some/file
630
  def parse_redmine_links(text, project, obj, attr, only_path, options)
631 632
    text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-]+):)?(attachment|document|version|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m|
      leading, esc, project_prefix, project_identifier, prefix, sep, identifier = $1, $2, $3, $4, $5, $7 || $9, $8 || $10
633
      link = nil
634 635 636
      if project_identifier
        project = Project.visible.find_by_identifier(project_identifier)
      end
637 638
      if esc.nil?
        if prefix.nil? && sep == 'r'
639 640
          # project.changesets.visible raises an SQL error because of a double join on repositories
          if project && project.repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(project.repository.id, identifier))
641
            link = link_to(h("#{project_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
jplang's avatar
jplang committed
642
                                      :class => 'changeset',
643
                                      :title => truncate_single_line(changeset.comments, :length => 100))
644 645
          end
        elsif sep == '#'
646
          oid = identifier.to_i
647 648
          case prefix
          when nil
jplang's avatar
jplang committed
649
            if issue = Issue.visible.find_by_id(oid, :include => :status)
jplang's avatar
jplang committed
650
              link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
jplang's avatar
jplang committed
651
                                        :class => issue.css_classes,
652
                                        :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
653 654
            end
          when 'document'
655
            if document = Document.visible.find_by_id(oid)
jplang's avatar
jplang committed
656 657
              link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
                                                :class => 'document'
658 659
            end
          when 'version'
660
            if version = Version.visible.find_by_id(oid)
jplang's avatar
jplang committed
661 662
              link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
                                              :class => 'version'
663
            end
664
          when 'message'
665
            if message = Message.visible.find_by_id(oid, :include => :parent)
666
              link = link_to_message(message, {:only_path => only_path}, :class => 'message')
667
            end
jplang's avatar
jplang committed
668 669
          when 'project'
            if p = Project.visible.find_by_id(oid)
670
              link = link_to_project(p, {:only_path => only_path}, :class => 'project')
jplang's avatar
jplang committed
671
            end
672 673 674
          end
        elsif sep == ':'
          # removes the double quotes if any
675
          name = identifier.gsub(%r{^"(.*)"$}, "\\1")
676 677
          case prefix
          when 'document'
678
            if project && document = project.documents.visible.find_by_title(name)
jplang's avatar
jplang committed
679 680
              link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
                                                :class => 'document'
681 682
            end
          when 'version'
683
            if project && version = project.versions.visible.find_by_name(name)
jplang's avatar
jplang committed
684 685
              link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
                                              :class => 'version'
686
            end
687
          when 'commit'
688
            if project && project.repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", project.repository.id, "#{name}%"]))
689
              link = link_to h("#{project_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.identifier},
690
                                           :class => 'changeset',
691
                                           :title => truncate_single_line(h(changeset.comments), :length => 100)
692 693
            end
          when 'source', 'export'
694
            if project && project.repository && User.current.allowed_to?(:browse_repository, project)
695 696
              name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
              path, rev, anchor = $1, $3, $5
697
              link = link_to h("#{project_prefix}#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
jplang's avatar
jplang committed
698
                                                      :path => to_path_param(path),
699 700 701 702
                                                      :rev => rev,
                                                      :anchor => anchor,
                                                      :format => (prefix == 'export' ? 'raw' : nil)},
                                                     :class => (prefix == 'export' ? 'source download' : 'source')
703
            end
704
          when 'attachment'
705
            attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
706
            if attachments && attachment = attachments.detect {|a| a.filename == name }
jplang's avatar
jplang committed
707 708
              link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
                                                     :class => 'attachment'
709
            end
jplang's avatar
jplang committed
710 711
          when 'project'
            if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
712
              link = link_to_project(p, {:only_path => only_path}, :class => 'project')
jplang's avatar
jplang committed
713
            end
714
          end
jplang's avatar
jplang committed
715
        end
716
      end
717
      (leading + (link || "#{project_prefix}#{prefix}#{sep}#{identifier}")).html_safe
718
    end
719
  end
720

721
  HEADING_RE = /<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>/i unless const_defined?(:HEADING_RE)
722

723
  # Headings and TOC
724
  # Adds ids and links to headings unless options[:headings] is set to false
725
  def parse_headings(text, project, obj, attr, only_path, options)
726
    return if options[:headings] == false
727

728
    text.gsub!(HEADING_RE) do
jplang's avatar
jplang committed
729
      level, attrs, content = $1.to_i, $2, $3
730
      item = strip_tags(content).strip
731
      anchor = sanitize_anchor_name(item)
732 733
      # used for single-file wiki export
      anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
734
      @parsed_headings << [level, anchor, item]
735
      "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
736 737
    end
  end
738

739
  TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
740

741 742
  # Renders the TOC with given headings
  def replace_toc(text, headings)
743 744 745 746 747 748 749
    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
750 751 752 753
        out = "<ul class=\"#{div_class}\"><li>"
        root = headings.map(&:first).min
        current = root
        started = false
754
        headings.each do |level, anchor, item|
jplang's avatar
jplang committed
755 756 757 758 759 760 761 762 763 764
          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
765
        end
jplang's avatar
jplang committed
766 767
        out << '</li></ul>' * (current - root)
        out << '</li></ul>'
768 769 770
      end
    end
  end
771

jplang's avatar
jplang committed
772 773 774 775 776 777 778
  # 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
779

780
  def lang_options_for_select(blank=true)
781
    (blank ? [["(auto)", ""]] : []) +
782
      valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
783
  end
784

785 786 787 788
  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
789

790 791
  def labelled_tabular_form_for(name, object, options, &proc)
    options[:html] ||= {}
792
    options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
793 794
    form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
  end
795

796
  def back_url_hidden_field_tag
797
    back_url = params[:back_url] || request.env['HTTP_REFERER']
798
    back_url = CGI.unescape(back_url.to_s)
799
    hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
800
  end
801

802 803
  def check_all_links(form_name)
    link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
804
    " | ".html_safe +
805
    link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
806
  end
807

808 809
  def progress_bar(pcts, options={})
    pcts = [pcts, pcts] unless pcts.is_a?(Array)
810
    pcts = pcts.collect(&:round)
811 812
    pcts[1] = pcts[1] - pcts[0]
    pcts << (100 - pcts[1] - pcts[0])
813 814 815 816
    width = options[:width] || '100px;'
    legend = options[:legend] || ''
    content_tag('table',
      content_tag('tr',
817 818 819 820 821
        (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
        (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
        (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
      ), :class => 'progress', :style => "width: #{width};").html_safe +
      content_tag('p', legend, :class => 'pourcent').html_safe
822
  end
823

824 825 826 827 828
  def checked_image(checked=true)
    if checked
      image_tag 'toggle_check.png'
    end
  end
829

830 831 832 833 834 835
  def context_menu(url)
    unless @context_menu_included
      content_for :header_tags do
        javascript_include_tag('context_menu') +
          stylesheet_link_tag('context_menu')
      end
836 837 838 839 840
      if l(:direction) == 'rtl'
        content_for :header_tags do
          stylesheet_link_tag('context_menu_rtl')
        end
      end
841 842 843 844
      @context_menu_included = true
    end
    javascript_tag "new ContextMenu('#{ url_for(url) }')"
  end
845

846 847 848 849 850 851 852 853 854 855 856 857 858
  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
859
    link_to h(name), url, options
860
  end
861

862
  def calendar_for(field_id)
863
    include_calendar_headers_tags
864 865 866
    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
867 868 869 870 871

  def include_calendar_headers_tags
    unless @calendar_headers_tags_included
      @calendar_headers_tags_included = true
      content_for :header_tags do
872 873 874 875 876
        start_of_week = case Setting.start_of_week.to_i
        when 1
          'Calendar._FD = 1;' # Monday
        when 7
          'Calendar._FD = 0;' # Sunday
877 878
        when 6
          'Calendar._FD = 6;' # Saturday
879 880 881
        else
          '' # use language
        end
882

883
        javascript_include_tag('calendar/calendar') +
884
        javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
885
        javascript_tag(start_of_week) +
886 887 888 889 890
        javascript_include_tag('calendar/calendar-setup') +
        stylesheet_link_tag('calendar')
      end
    end
  end
891

jplang's avatar
jplang committed
892 893 894 895 896
  def content_for(name, content = nil, &block)
    @has_content ||= {}
    @has_content[name] = true
    super(name, content, &block)
  end
897

jplang's avatar
jplang committed
898 899 900
  def has_content?(name)
    (@has_content && @has_content</