GitLab steht Mittwoch, den 23. September, zwischen 10:00 und 12:00 Uhr aufgrund von Wartungsarbeiten nicht zur Verfügung.

application_helper.rb 34.3 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
  # 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 110
    link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => rev},
            :title => l(:label_revision_id, format_revision(revision)))
111 112
  end

113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129
  # 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
130 131 132 133 134 135
  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
136

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

145 146 147 148
  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
149 150
  
  def format_activity_title(text)
151
    h(truncate_single_line(text, :length => 100))
152 153 154 155 156 157 158
  end
  
  def format_activity_day(date)
    date == Date.today ? l(:label_today).titleize : format_date(date)
  end
  
  def format_activity_description(text)
159
    h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
160
  end
161

162 163 164 165 166 167 168 169
  def format_version_name(version)
    if version.project == @project
    	h(version)
    else
      h("#{version.project} - #{version}")
    end
  end
  
170 171 172 173 174
  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
175

176 177 178 179 180 181
  def render_page_hierarchy(pages, node=nil)
    content = ''
    if pages[node]
      content << "<ul class=\"pages-hierarchy\">\n"
      pages[node].each do |page|
        content << "<li>"
182
        content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
183 184 185 186 187 188 189 190
                           :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
191 192 193 194 195 196 197 198 199
  
  # Renders flash messages
  def render_flash_messages
    s = ''
    flash.each do |k,v|
      s << content_tag('div', v, :class => "flash #{k}")
    end
    s
  end
200
  
jplang's avatar
jplang committed
201 202 203 204 205 206 207 208 209
  # 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
  
210 211 212 213 214 215
  # 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; }">' +
216
            "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
jplang's avatar
jplang committed
217
            '<option value="" disabled="disabled">---</option>'
218
      s << project_tree_options_for_select(projects, :selected => @project) do |p|
219
        { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
220 221 222 223 224 225 226 227 228 229
      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; ') : '')
230 231 232 233 234 235
      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
236 237 238 239 240 241 242
      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
243 244
  #
  # Wrapper for Project#project_tree
245
  def project_tree(projects, &block)
246
    Project.project_tree(projects, &block)
247
  end
248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271
  
  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
272 273 274
  
  def principals_check_box_tags(name, principals)
    s = ''
275
    principals.sort.each do |principal|
jplang's avatar
jplang committed
276 277 278 279
      s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
    end
    s 
  end
280

281 282
  # Truncates and returns the string as a single line
  def truncate_single_line(string, *args)
283
    truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
284
  end
285 286 287 288 289 290 291 292 293 294
  
  # 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
295

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

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

313
  def syntax_highlight(name, content)
314
    Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
315
  end
316

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

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

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

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

341
    unless count.nil?
342 343 344 345
      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
346
    end
347 348

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

355
    links = Setting.per_page_options_array.collect do |n|
356 357 358
      n == selected ? n : link_to_remote(n, {:update => "content",
                                             :url => params.dup.merge(:per_page => n),
                                             :method => :get},
359
                                            {:href => url_for(url_param.merge(:per_page => n))})
360 361 362
    end
    links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
  end
jplang's avatar
jplang committed
363 364 365 366 367 368 369
  
  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
370

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

402 403 404 405 406 407
  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
408
      title.select {|t| !t.blank? }.join(' - ')
409 410 411 412
    else
      @html_title ||= []
      @html_title += args
    end
413
  end
jplang's avatar
jplang committed
414

415 416 417 418 419 420 421 422 423 424 425 426 427
  # 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
428
  def accesskey(s)
429
    Redmine::AccessKeys.key_for s
jplang's avatar
jplang committed
430 431
  end

432 433 434 435 436 437 438 439
  # 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
440
      obj = options[:object]
jplang's avatar
jplang committed
441
      text = args.shift
442 443
    when 2
      obj = args.shift
444 445
      attr = args.shift
      text = obj.send(attr).to_s
446 447 448
    else
      raise ArgumentError, 'invalid arguments to textilizable'
    end
jplang's avatar
jplang committed
449
    return '' if text.blank?
450 451
    project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
    only_path = options.delete(:only_path) == false ? false : true
452

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

520 521 522 523 524 525 526 527 528 529 530 531
  # 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|
532
      link_project = project
533 534 535
      esc, all, page, title = $1, $2, $3, $5
      if esc.nil?
        if page =~ /^([^\:]+)\:(.*)$/
536
          link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
537 538 539
          page = $2
          title ||= $1 if page.blank?
        end
540

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

jplang's avatar
jplang committed
735 736 737 738 739 740 741
  # 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
742

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

748 749 750 751
  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
752

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

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

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

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

809 810 811 812 813 814 815 816 817 818 819 820 821 822 823
  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
824

825
  def calendar_for(field_id)
826
    include_calendar_headers_tags
827 828 829
    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
830 831 832 833 834

  def include_calendar_headers_tags
    unless @calendar_headers_tags_included
      @calendar_headers_tags_included = true
      content_for :header_tags do
835 836 837 838 839 840 841 842 843
        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
        
844
        javascript_include_tag('calendar/calendar') +
845
        javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
846
        javascript_tag(start_of_week) +  
847 848 849 850 851
        javascript_include_tag('calendar/calendar-setup') +
        stylesheet_link_tag('calendar')
      end
    end
  end
852

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

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

863 864 865
  # 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 = { })
866
    if Setting.gravatar_enabled?
867
      options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
868 869 870 871 872 873 874
      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
875 876
    else
      ''
877 878 879
    end
  end

880
  def favicon
881
    "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />"
882
  end
883 884 885 886 887 888 889 890 891 892
  
  # 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
893

894 895 896 897 898 899 900 901 902 903 904 905
  # 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
  
906
  private
907

908 909 910 911 912
  def wiki_helper
    helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
    extend helper
    return self
  end
913 914 915 916 917 918 919 920
  
  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
921
end