application_helper.rb 36.5 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
        content << "</li>\n"
      end
      content << "</ul>\n"
    end
203
    content.html_safe
204
  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
  def reorder_links(name, url)
391 392 393 394 395 396 397 398 399 400 401 402
    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))
jplang's avatar
jplang committed
403
  end
404

405
  def breadcrumb(*args)
406
    elements = args.flatten
407
    elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
408
  end
409

410
  def other_formats_links(&block)
411
    concat('<p class="other-formats">' + l(:label_export_to))
412
    yield Redmine::Views::OtherFormatsBuilder.new(self)
413
    concat('</p>')
414
  end
415

416 417 418 419 420
  def page_header_title
    if @project.nil? || @project.new_record?
      h(Setting.app_title)
    else
      b = []
jplang's avatar
jplang committed
421
      ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
422 423
      if ancestors.any?
        root = ancestors.shift
424
        b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
425
        if ancestors.size > 2
426
          b << "\xe2\x80\xa6"
427 428
          ancestors = ancestors[-2, 2]
        end
429
        b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
430 431
      end
      b << h(@project)
432
      b.join(" \xc2\xbb ").html_safe
433 434
    end
  end
435

436 437 438
  def html_title(*args)
    if args.empty?
      title = []
tmaruyama's avatar
tmaruyama committed
439
      title << @project.name if @project
440 441
      title += @html_title if @html_title
      title << Setting.app_title
jplang's avatar
jplang committed
442
      title.select {|t| !t.blank? }.join(' - ')
443 444 445 446
    else
      @html_title ||= []
      @html_title += args
    end
447
  end
jplang's avatar
jplang committed
448

449 450 451 452 453 454 455 456 457 458 459 460 461
  # 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
462
  def accesskey(s)
463
    Redmine::AccessKeys.key_for s
jplang's avatar
jplang committed
464 465
  end

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

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

489 490
    @parsed_headings = []
    text = parse_non_pre_blocks(text) do |text|
491
      [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_headings].each do |method_name|
492 493
        send method_name, text, project, obj, attr, only_path, options
      end
494
    end
495

496 497 498
    if @parsed_headings.any?
      replace_toc(text, @parsed_headings)
    end
499

500
    text
501
  end
502

503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524
  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
525 526 527 528
    # Close any non closing tags
    while tag = tags.pop
      parsed << "</#{tag}>"
    end
529
    parsed.html_safe
530
  end
531

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

554 555 556 557 558 559 560 561 562 563 564 565
  # 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|
566
      link_project = project
567 568 569
      esc, all, page, title = $1, $2, $3, $5
      if esc.nil?
        if page =~ /^([^\:]+)\:(.*)$/
570
          link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
571 572 573
          page = $2
          title ||= $1 if page.blank?
        end
574

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

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

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

731
  # Headings and TOC
732
  # Adds ids and links to headings unless options[:headings] is set to false
733
  def parse_headings(text, project, obj, attr, only_path, options)
734
    return if options[:headings] == false
735

736
    text.gsub!(HEADING_RE) do
jplang's avatar
jplang committed
737
      level, attrs, content = $1.to_i, $2, $3
738
      item = strip_tags(content).strip
739
      anchor = sanitize_anchor_name(item)
740 741
      # 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))
742
      @parsed_headings << [level, anchor, item]
743
      "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
744 745
    end
  end
746

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

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

jplang's avatar
jplang committed
780 781 782 783 784
  # 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
785 786
      gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline   -> br
      html_safe
jplang's avatar
jplang committed
787
  end
788

789
  def lang_options_for_select(blank=true)
790
    (blank ? [["(auto)", ""]] : []) +
791
      valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
792
  end
793

794 795 796 797
  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
798

799 800
  def labelled_tabular_form_for(name, object, options, &proc)
    options[:html] ||= {}
801
    options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
802 803
    form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
  end
804

805
  def back_url_hidden_field_tag
806
    back_url = params[:back_url] || request.env['HTTP_REFERER']
807
    back_url = CGI.unescape(back_url.to_s)
808
    hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
809
  end
810

811 812
  def check_all_links(form_name)
    link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
813
    " | ".html_safe +
814
    link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
815
  end
816

817 818
  def progress_bar(pcts, options={})
    pcts = [pcts, pcts] unless pcts.is_a?(Array)
819
    pcts = pcts.collect(&:round)
820 821
    pcts[1] = pcts[1] - pcts[0]
    pcts << (100 - pcts[1] - pcts[0])
822 823 824 825
    width = options[:width] || '100px;'
    legend = options[:legend] || ''
    content_tag('table',
      content_tag('tr',
826 827 828 829 830
        (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
831
  end
832

833 834 835 836 837
  def checked_image(checked=true)
    if checked
      image_tag 'toggle_check.png'
    end
  end
838

839 840 841 842 843 844
  def context_menu(url)
    unless @context_menu_included
      content_for :header_tags do
        javascript_include_tag('context_menu') +
          stylesheet_link_tag('context_menu')
      end
845 846 847 848 849
      if l(:direction) == 'rtl'
        content_for :header_tags do
          stylesheet_link_tag('context_menu_rtl')
        end
      end
850 851 852 853
      @context_menu_included = true
    end
    javascript_tag "new ContextMenu('#{ url_for(url) }')"
  end
854

855 856 857 858 859 860 861 862 863 864 865 866 867
  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
868
    link_to h(name), url, options
869
  end
870

871
  def calendar_for(field_id)
872
    include_calendar_headers_tags
873 874 875
    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
876 877 878 879 880

  def include_calendar_headers_tags
    unless @calendar_headers_tags_included
      @calendar_headers_tags_included = true
      content_for :header_tags do
881 882 883 884 885
        start_of_week = case Setting.start_of_week.to_i
        when 1
          'Calendar._FD = 1;' # Monday
        when 7
          'Calendar._FD = 0;' # Sunday
886 887
        when 6
          'Calendar._FD = 6;' # Saturday
888 889 890
        else
          '' # use language
        end
891

jplang's avatar