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

application_helper.rb 29.5 KB
Newer Older
1 2 3 4 5 6 7
# redMine - project management software
# Copyright (C) 2006-2007  Jean-Philippe Lang
#
# 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 35
  end

  # Display a link if user is authorized
  def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
jplang's avatar
jplang committed
36
    link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
37
  end
38

39 40 41 42
  # 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])
43 44
  end

45
  # Displays a link to user's account page if active
46
  def link_to_user(user, options={})
jplang's avatar
jplang committed
47
    if user.is_a?(User)
48 49 50 51 52 53
      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
54
    else
55
      h(user.to_s)
jplang's avatar
jplang committed
56
    end
57
  end
58

59 60 61 62 63 64
  # 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
65
  #   link_to_issue(issue, :project => true)      # => Foo - Defect #6
66
  #
67
  def link_to_issue(issue, options={})
68 69 70 71 72 73 74 75 76 77 78 79 80 81
    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
82
    s = "#{h issue.project} - " + s if options[:project]
83
    s
jplang's avatar
jplang committed
84
  end
85

86 87 88 89 90 91 92
  # 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'
93

94 95
    link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
  end
96

97 98 99 100 101 102 103 104 105
  # Generates a link to a SCM revision
  # Options:
  # * :text - Link text (default to the formatted revision)
  def link_to_revision(revision, project, options={})
    text = options.delete(:text) || format_revision(revision)

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

jplang's avatar
jplang committed
106 107 108 109 110 111
  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
112

113 114
  def image_to_function(name, function, html_options = {})
    html_options.symbolize_keys!
115 116 117
    tag(:input, html_options.merge({
        :type => "image", :src => image_path(name),
        :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
118 119
        }))
  end
120

121 122 123 124
  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
125 126
  
  def format_activity_title(text)
127
    h(truncate_single_line(text, :length => 100))
128 129 130 131 132 133 134
  end
  
  def format_activity_day(date)
    date == Date.today ? l(:label_today).titleize : format_date(date)
  end
  
  def format_activity_description(text)
135
    h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
136
  end
137

138 139 140 141 142 143 144 145
  def format_version_name(version)
    if version.project == @project
    	h(version)
    else
      h("#{version.project} - #{version}")
    end
  end
  
146 147 148 149 150
  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
151

152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
  def render_page_hierarchy(pages, node=nil)
    content = ''
    if pages[node]
      content << "<ul class=\"pages-hierarchy\">\n"
      pages[node].each do |page|
        content << "<li>"
        content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'index', :id => page.project, :page => page.title},
                           :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
167 168 169 170 171 172 173 174 175
  
  # Renders flash messages
  def render_flash_messages
    s = ''
    flash.each do |k,v|
      s << content_tag('div', v, :class => "flash #{k}")
    end
    s
  end
176
  
jplang's avatar
jplang committed
177 178 179 180 181 182 183 184 185
  # 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
  
186 187 188 189 190 191
  # 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; }">' +
192
            "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
jplang's avatar
jplang committed
193
            '<option value="" disabled="disabled">---</option>'
194
      s << project_tree_options_for_select(projects, :selected => @project) do |p|
195
        { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
196 197 198 199 200 201 202 203 204 205
      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; ') : '')
206 207 208 209 210 211
      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
212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228
      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
  def project_tree(projects, &block)
    ancestors = []
    projects.sort_by(&:lft).each do |project|
      while (ancestors.any? && !project.is_descendant_of?(ancestors.last)) 
        ancestors.pop
      end
      yield project, ancestors.size
      ancestors << project
    end
  end
229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252
  
  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
253 254 255
  
  def principals_check_box_tags(name, principals)
    s = ''
256
    principals.sort.each do |principal|
jplang's avatar
jplang committed
257 258 259 260
      s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
    end
    s 
  end
261

262 263
  # Truncates and returns the string as a single line
  def truncate_single_line(string, *args)
264
    truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
265
  end
266

267 268 269
  def html_hours(text)
    text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
  end
270

jplang's avatar
jplang committed
271
  def authoring(created, author, options={})
272
    l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created))
273 274 275 276 277 278 279 280 281
  end
  
  def time_tag(time)
    text = distance_of_time_in_words(Time.now, time)
    if @project
      link_to(text, {:controller => 'projects', :action => 'activity', :id => @project, :from => time.to_date}, :title => format_time(time))
    else
      content_tag('acronym', text, :title => format_time(time))
    end
282 283
  end

284
  def syntax_highlight(name, content)
285
    Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
286
  end
287

jplang's avatar
jplang committed
288 289 290 291
  def to_path_param(path)
    path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
  end

292
  def pagination_links_full(paginator, count=nil, options={})
293
    page_param = options.delete(:page_param) || :page
294
    per_page_links = options.delete(:per_page_links)
295
    url_param = params.dup
296 297
    # don't reuse query params if filters are present
    url_param.merge!(:fields => nil, :values => nil, :operators => nil) if url_param.delete(:set_filter)
298 299

    html = ''
300 301 302
    if paginator.current.previous
      html << link_to_remote_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
    end
303

304
    html << (pagination_links_each(paginator, options) do |n|
305
      link_to_remote_content_update(n.to_s, url_param.merge(page_param => n))
306
    end || '')
307 308 309 310
    
    if paginator.current.next
      html << ' ' + link_to_remote_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next))
    end
311

312
    unless count.nil?
313 314 315 316
      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
317
    end
318 319

    html
320
  end
321
  
322
  def per_page_links(selected=nil)
323 324
    url_param = params.dup
    url_param.clear if url_param.has_key?(:set_filter)
325

326
    links = Setting.per_page_options_array.collect do |n|
327 328 329
      n == selected ? n : link_to_remote(n, {:update => "content",
                                             :url => params.dup.merge(:per_page => n),
                                             :method => :get},
330
                                            {:href => url_for(url_param.merge(:per_page => n))})
331 332 333
    end
    links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
  end
jplang's avatar
jplang committed
334 335 336 337 338 339 340
  
  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
341

342
  def breadcrumb(*args)
343 344
    elements = args.flatten
    elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
345
  end
346 347
  
  def other_formats_links(&block)
348
    concat('<p class="other-formats">' + l(:label_export_to))
349
    yield Redmine::Views::OtherFormatsBuilder.new(self)
350
    concat('</p>')
351
  end
352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371
  
  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
        b << link_to(h(root), {:controller => 'projects', :action => 'show', :id => root, :jump => current_menu_item}, :class => 'root')
        if ancestors.size > 2
          b << '&#8230;'
          ancestors = ancestors[-2, 2]
        end
        b += ancestors.collect {|p| link_to(h(p), {:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item}, :class => 'ancestor') }
      end
      b << h(@project)
      b.join(' &#187; ')
    end
  end
372

373 374 375 376 377 378
  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
379
      title.select {|t| !t.blank? }.join(' - ')
380 381 382 383
    else
      @html_title ||= []
      @html_title += args
    end
384
  end
jplang's avatar
jplang committed
385 386

  def accesskey(s)
387
    Redmine::AccessKeys.key_for s
jplang's avatar
jplang committed
388 389
  end

390 391 392 393 394 395 396 397
  # 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
398
      obj = options[:object]
jplang's avatar
jplang committed
399
      text = args.shift
400 401
    when 2
      obj = args.shift
402 403
      attr = args.shift
      text = obj.send(attr).to_s
404 405 406
    else
      raise ArgumentError, 'invalid arguments to textilizable'
    end
jplang's avatar
jplang committed
407
    return '' if text.blank?
408 409
    project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
    only_path = options.delete(:only_path) == false ? false : true
410

411
    text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }
412
    
413 414 415 416 417 418 419 420
    [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
      send method_name, text, project, obj, attr, only_path, options
    end
    
    text
  end
  
  def parse_inline_attachments(text, project, obj, attr, only_path, options)
421
    # when using an image link, try to use an attachment, if possible
422 423
    if options[:attachments] || (obj && obj.respond_to?(:attachments))
      attachments = nil
424 425
      text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
        filename, ext, alt, alttext = $1.downcase, $2, $3, $4 
426
        attachments ||= (options[:attachments] || obj.attachments).sort_by(&:created_on).reverse
427
        # search for the picture in attachments
428
        if found = attachments.detect { |att| att.filename.downcase == filename }
429
          image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
430 431 432 433 434
          desc = found.description.to_s.gsub('"', '')
          if !desc.blank? && alttext.blank?
            alt = " title=\"#{desc}\" alt=\"#{desc}\""
          end
          "src=\"#{image_url}\"#{alt}"
435
        else
436
          m
437 438 439
        end
      end
    end
440
  end
441

442 443 444 445 446 447 448 449 450 451 452 453
  # 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|
454
      link_project = project
455 456 457 458 459 460 461
      esc, all, page, title = $1, $2, $3, $5
      if esc.nil?
        if page =~ /^([^\:]+)\:(.*)$/
          link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
          page = $2
          title ||= $1 if page.blank?
        end
462

463
        if link_project && link_project.wiki
464 465 466 467 468
          # extract anchor
          anchor = nil
          if page =~ /^(.+?)\#(.+)$/
            page, anchor = $1, $2
          end
469 470
          # check if page exists
          wiki_page = link_project.wiki.find_page(page)
471 472 473 474 475 476 477
          url = case options[:wiki_links]
            when :local; "#{title}.html"
            when :anchor; "##{title}"   # used for single-file wiki export
            else
              url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => link_project, :page => Wiki.titleize(page), :anchor => anchor)
            end
          link_to((title || page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
478 479
        else
          # project or wiki doesn't exist
480
          all
481
        end
jplang's avatar
jplang committed
482
      else
483
        all
jplang's avatar
jplang committed
484
      end
485
    end
486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515
  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)
    text.gsub!(%r{([\s\(,\-\>]|^)(!)?(attachment|document|version|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|<|$)}) do |m|
516
      leading, esc, prefix, sep, identifier = $1, $2, $3, $5 || $7, $6 || $8
517
      link = nil
518 519
      if esc.nil?
        if prefix.nil? && sep == 'r'
520 521
          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
522
                                      :class => 'changeset',
523
                                      :title => truncate_single_line(changeset.comments, :length => 100))
524 525
          end
        elsif sep == '#'
526
          oid = identifier.to_i
527 528
          case prefix
          when nil
jplang's avatar
jplang committed
529
            if issue = Issue.visible.find_by_id(oid, :include => :status)
jplang's avatar
jplang committed
530
              link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
jplang's avatar
jplang committed
531
                                        :class => issue.css_classes,
532
                                        :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
533 534 535
            end
          when 'document'
            if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
jplang's avatar
jplang committed
536 537
              link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
                                                :class => 'document'
538 539 540
            end
          when 'version'
            if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
jplang's avatar
jplang committed
541 542
              link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
                                              :class => 'version'
543
            end
544 545
          when 'message'
            if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
546
              link = link_to h(truncate(message.subject, :length => 60)), {:only_path => only_path,
547 548 549 550 551 552 553
                                                                :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
554 555 556 557 558
          when 'project'
            if p = Project.visible.find_by_id(oid)
              link = link_to h(p.name), {:only_path => only_path, :controller => 'projects', :action => 'show', :id => p},
                                              :class => 'project'
            end
559 560 561
          end
        elsif sep == ':'
          # removes the double quotes if any
562
          name = identifier.gsub(%r{^"(.*)"$}, "\\1")
563 564 565
          case prefix
          when 'document'
            if project && document = project.documents.find_by_title(name)
jplang's avatar
jplang committed
566 567
              link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
                                                :class => 'document'
568 569 570
            end
          when 'version'
            if project && version = project.versions.find_by_name(name)
jplang's avatar
jplang committed
571 572
              link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
                                              :class => 'version'
573
            end
574 575
          when 'commit'
            if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
576 577
              link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
                                           :class => 'changeset',
578
                                           :title => truncate_single_line(changeset.comments, :length => 100)
579 580 581 582 583
            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
584 585
              link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
                                                      :path => to_path_param(path),
586 587 588 589
                                                      :rev => rev,
                                                      :anchor => anchor,
                                                      :format => (prefix == 'export' ? 'raw' : nil)},
                                                     :class => (prefix == 'export' ? 'source download' : 'source')
590
            end
591
          when 'attachment'
592
            attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
593
            if attachments && attachment = attachments.detect {|a| a.filename == name }
jplang's avatar
jplang committed
594 595
              link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
                                                     :class => 'attachment'
596
            end
jplang's avatar
jplang committed
597 598 599 600 601
          when 'project'
            if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
              link = link_to h(p.name), {:only_path => only_path, :controller => 'projects', :action => 'show', :id => p},
                                              :class => 'project'
            end
602
          end
jplang's avatar
jplang committed
603
        end
604
      end
605
      leading + (link || "#{prefix}#{sep}#{identifier}")
606
    end
607
  end
608

jplang's avatar
jplang committed
609 610 611 612 613 614 615
  # 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
616

617
  def lang_options_for_select(blank=true)
618
    (blank ? [["(auto)", ""]] : []) +
619
      valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
620
  end
621

622 623 624 625
  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
626

627 628
  def labelled_tabular_form_for(name, object, options, &proc)
    options[:html] ||= {}
629
    options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
630 631
    form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
  end
632

633
  def back_url_hidden_field_tag
634
    back_url = params[:back_url] || request.env['HTTP_REFERER']
635
    back_url = CGI.unescape(back_url.to_s)
636
    hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
637
  end
638

639 640 641
  def check_all_links(form_name)
    link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
    " | " +
642
    link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
643
  end
644

645 646
  def progress_bar(pcts, options={})
    pcts = [pcts, pcts] unless pcts.is_a?(Array)
647
    pcts = pcts.collect(&:round)
648 649
    pcts[1] = pcts[1] - pcts[0]
    pcts << (100 - pcts[1] - pcts[0])
650 651 652 653
    width = options[:width] || '100px;'
    legend = options[:legend] || ''
    content_tag('table',
      content_tag('tr',
654 655 656
        (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') : '')
657 658 659
      ), :class => 'progress', :style => "width: #{width};") +
      content_tag('p', legend, :class => 'pourcent')
  end
660 661 662 663 664 665 666 667 668 669 670
  
  def context_menu(url)
    unless @context_menu_included
      content_for :header_tags do
        javascript_include_tag('context_menu') +
          stylesheet_link_tag('context_menu')
      end
      @context_menu_included = true
    end
    javascript_tag "new ContextMenu('#{ url_for(url) }')"
  end
671

672 673 674 675 676 677 678 679 680 681 682 683 684 685 686
  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
687

688
  def calendar_for(field_id)
689
    include_calendar_headers_tags
690 691 692
    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
693 694 695 696 697

  def include_calendar_headers_tags
    unless @calendar_headers_tags_included
      @calendar_headers_tags_included = true
      content_for :header_tags do
698 699 700 701 702 703 704 705 706
        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
        
707
        javascript_include_tag('calendar/calendar') +
708
        javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
709
        javascript_tag(start_of_week) +  
710 711 712 713 714
        javascript_include_tag('calendar/calendar-setup') +
        stylesheet_link_tag('calendar')
      end
    end
  end
715

jplang's avatar
jplang committed
716 717 718 719 720
  def content_for(name, content = nil, &block)
    @has_content ||= {}
    @has_content[name] = true
    super(name, content, &block)
  end
721

jplang's avatar
jplang committed
722 723 724
  def has_content?(name)
    (@has_content && @has_content[name]) || false
  end
725

726 727 728
  # 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 = { })
729
    if Setting.gravatar_enabled?
730
      options.merge!({:ssl => Setting.protocol == 'https', :default => Setting.gravatar_default})
731 732 733 734 735 736 737
      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
738 739 740
    end
  end

741
  private
742

743 744 745 746 747
  def wiki_helper
    helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
    extend helper
    return self
  end
748 749 750 751 752 753 754 755
  
  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
756
end