application_helper.rb 40 KB
Newer Older
1 2
# encoding: utf-8
#
3
# Redmine - project management software
4
# Copyright (C) 2006-2011  Jean-Philippe Lang
5 6 7 8 9
#
# 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.
10
#
11 12 13 14
# 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.
15
#
16 17 18 19
# 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.

20
require 'forwardable'
21
require 'cgi'
22

23
module ApplicationHelper
24
  include Redmine::WikiFormatting::Macros::Definitions
25
  include Redmine::I18n
26
  include GravatarHelper::PublicMethods
27

28 29 30
  extend Forwardable
  def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter

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

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

46 47 48 49
  # 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])
50 51
  end

52
  # Displays a link to user's account page if active
53
  def link_to_user(user, options={})
jplang's avatar
jplang committed
54
    if user.is_a?(User)
55 56 57 58 59 60
      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
61
    else
62
      h(user.to_s)
jplang's avatar
jplang committed
63
    end
64
  end
65

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

93 94 95 96 97 98 99
  # 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'
100 101 102 103
    link_to(h(text),
           {:controller => 'attachments', :action => action,
            :id => attachment, :filename => attachment.filename },
           options)
104
  end
105

106 107 108 109 110
  # Generates a link to a SCM revision
  # Options:
  # * :text - Link text (default to the formatted revision)
  def link_to_revision(revision, project, options={})
    text = options.delete(:text) || format_revision(revision)
111
    rev = revision.respond_to?(:identifier) ? revision.identifier : revision
112 113 114 115 116
    link_to(
        h(text),
        {:controller => 'repositories', :action => 'revision', :id => project, :rev => rev},
        :title => l(:label_revision_id, format_revision(revision))
      )
117
  end
118

119 120 121 122 123 124 125 126 127 128 129 130 131
  # 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
132

133 134
  # Generates a link to a project if active
  # Examples:
135
  #
136 137 138 139 140 141 142 143 144 145 146 147 148 149
  #   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
150 151 152 153 154 155
  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
156

157 158
  def image_to_function(name, function, html_options = {})
    html_options.symbolize_keys!
159 160 161
    tag(:input, html_options.merge({
        :type => "image", :src => image_path(name),
        :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
162 163
        }))
  end
164

165 166 167 168
  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
169

170
  def format_activity_title(text)
171
    h(truncate_single_line(text, :length => 100))
172
  end
173

174 175 176
  def format_activity_day(date)
    date == Date.today ? l(:label_today).titleize : format_date(date)
  end
177

178
  def format_activity_description(text)
179 180
    h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
       ).gsub(/[\r\n]+/, "<br />").html_safe
181
  end
182

183 184 185 186 187 188 189
  def format_version_name(version)
    if version.project == @project
    	h(version)
    else
      h("#{version.project} - #{version}")
    end
  end
190

191 192 193 194 195
  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
196

197
  def render_page_hierarchy(pages, node=nil, options={})
198 199 200 201 202
    content = ''
    if pages[node]
      content << "<ul class=\"pages-hierarchy\">\n"
      pages[node].each do |page|
        content << "<li>"
203
        content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
204 205
                           :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]
206 207 208 209
        content << "</li>\n"
      end
      content << "</ul>\n"
    end
210
    content.html_safe
211
  end
212

213 214 215 216
  # Renders flash messages
  def render_flash_messages
    s = ''
    flash.each do |k,v|
217
      s << (content_tag('div', v.html_safe, :class => "flash #{k}"))
218
    end
219
    s.html_safe
220
  end
221

jplang's avatar
jplang committed
222 223 224 225 226 227 228 229
  # 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
230

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

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

263
  # Yields the given block for each project with its level in the tree
264 265
  #
  # Wrapper for Project#project_tree
266
  def project_tree(projects, &block)
267
    Project.project_tree(projects, &block)
268
  end
269

270 271 272 273 274 275 276 277 278 279
  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>"
280
          while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
281 282 283 284 285 286 287 288 289 290
            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
291
    s.html_safe
292
  end
293

jplang's avatar
jplang committed
294 295
  def principals_check_box_tags(name, principals)
    s = ''
296
    principals.sort.each do |principal|
jplang's avatar
jplang committed
297 298
      s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
    end
299
    s.html_safe
jplang's avatar
jplang committed
300
  end
301

302 303 304 305 306 307 308 309 310 311 312 313 314
  # 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
315

316 317
  # Truncates and returns the string as a single line
  def truncate_single_line(string, *args)
318
    truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
319
  end
320

321 322 323 324 325 326 327 328 329
  # 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
330

331
  def html_hours(text)
332
    text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
333
  end
334

jplang's avatar
jplang committed
335
  def authoring(created, author, options={})
336
    l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
337
  end
338

339 340 341
  def time_tag(time)
    text = distance_of_time_in_words(Time.now, time)
    if @project
342
      link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
343 344 345
    else
      content_tag('acronym', text, :title => format_time(time))
    end
346 347
  end

348
  def syntax_highlight(name, content)
349
    Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
350
  end
351

jplang's avatar
jplang committed
352 353 354 355
  def to_path_param(path)
    path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
  end

356
  def pagination_links_full(paginator, count=nil, options={})
357
    page_param = options.delete(:page_param) || :page
358
    per_page_links = options.delete(:per_page_links)
359
    url_param = params.dup
360 361

    html = ''
362
    if paginator.current.previous
363 364 365 366
      # \xc2\xab(utf-8) = &#171;
      html << link_to_content_update(
                   "\xc2\xab " + l(:label_previous),
                   url_param.merge(page_param => paginator.current.previous)) + ' '
367
    end
368

369
    html << (pagination_links_each(paginator, options) do |n|
370
      link_to_content_update(n.to_s, url_param.merge(page_param => n))
371
    end || '')
372

373
    if paginator.current.next
374 375 376 377
      # \xc2\xbb(utf-8) = &#187;
      html << ' ' + link_to_content_update(
                      (l(:label_next) + " \xc2\xbb"),
                      url_param.merge(page_param => paginator.current.next))
378
    end
379

380
    unless count.nil?
381 382 383 384
      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
385
    end
386

387
    html.html_safe
388
  end
389

390 391
  def per_page_links(selected=nil)
    links = Setting.per_page_options_array.collect do |n|
392
      n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
393 394 395
    end
    links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
  end
396

jplang's avatar
jplang committed
397
  def reorder_links(name, url, method = :post)
398 399
    link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
            url.merge({"#{name}[move_to]" => 'highest'}),
jplang's avatar
jplang committed
400
            :method => method, :title => l(:label_sort_highest)) +
401 402
    link_to(image_tag('1uparrow.png',   :alt => l(:label_sort_higher)),
            url.merge({"#{name}[move_to]" => 'higher'}),
jplang's avatar
jplang committed
403
           :method => method, :title => l(:label_sort_higher)) +
404 405
    link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
            url.merge({"#{name}[move_to]" => 'lower'}),
jplang's avatar
jplang committed
406
            :method => method, :title => l(:label_sort_lower)) +
407 408
    link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
            url.merge({"#{name}[move_to]" => 'lowest'}),
jplang's avatar
jplang committed
409
           :method => method, :title => l(:label_sort_lowest))
jplang's avatar
jplang committed
410
  end
411

412
  def breadcrumb(*args)
413
    elements = args.flatten
414
    elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
415
  end
416

417
  def other_formats_links(&block)
jplang's avatar
jplang committed
418
    concat('<p class="other-formats">'.html_safe + l(:label_export_to))
419
    yield Redmine::Views::OtherFormatsBuilder.new(self)
jplang's avatar
jplang committed
420
    concat('</p>'.html_safe)
421
  end
422

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

443 444
  def html_title(*args)
    if args.empty?
445
      title = @html_title || []
tmaruyama's avatar
tmaruyama committed
446
      title << @project.name if @project
447
      title << Setting.app_title unless Setting.app_title == title.last
jplang's avatar
jplang committed
448
      title.select {|t| !t.blank? }.join(' - ')
449 450 451 452
    else
      @html_title ||= []
      @html_title += args
    end
453
  end
jplang's avatar
jplang committed
454

455 456 457 458 459 460 461 462 463 464 465 466 467
  # 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
468
  def accesskey(s)
469
    Redmine::AccessKeys.key_for s
jplang's avatar
jplang committed
470 471
  end

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

493
    text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
494

495
    @parsed_headings = []
496
    @current_section = 0 if options[:edit_section_links]
497
    text = parse_non_pre_blocks(text) do |text|
498
      [:parse_sections, :parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_macros, :parse_headings].each do |method_name|
499 500
        send method_name, text, project, obj, attr, only_path, options
      end
501
    end
502

503 504 505
    if @parsed_headings.any?
      replace_toc(text, @parsed_headings)
    end
506

507
    text
508
  end
509

510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531
  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
532 533 534 535
    # Close any non closing tags
    while tag = tags.pop
      parsed << "</#{tag}>"
    end
536
    parsed.html_safe
537
  end
538

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

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

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

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

756 757 758 759 760
  HEADING_RE = /(<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>)/i unless const_defined?(:HEADING_RE)

  def parse_sections(text, project, obj, attr, only_path, options)
    return unless options[:edit_section_links]
    text.gsub!(HEADING_RE) do
761 762
      @current_section += 1
      if @current_section > 1
763
        content_tag('div',
764
          link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
765 766 767 768 769 770 771
          :class => 'contextual',
          :title => l(:button_edit_section)) + $1
      else
        $1
      end
    end
  end
772

773
  # Headings and TOC
774
  # Adds ids and links to headings unless options[:headings] is set to false
775
  def parse_headings(text, project, obj, attr, only_path, options)
776
    return if options[:headings] == false
777

778
    text.gsub!(HEADING_RE) do
779
      level, attrs, content = $2.to_i, $3, $4
780
      item = strip_tags(content).strip
781
      anchor = sanitize_anchor_name(item)
782 783
      # 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))
784
      @parsed_headings << [level, anchor, item]
785
      "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
786 787
    end
  end
788

789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815
  MACROS_RE = /
                (!)?                        # escaping
                (
                \{\{                        # opening tag
                ([\w]+)                     # macro name
                (\(([^\}]*)\))?             # optional arguments
                \}\}                        # closing tag
                )
              /x unless const_defined?(:MACROS_RE)

  # Macros substitution
  def parse_macros(text, project, obj, attr, only_path, options)
    text.gsub!(MACROS_RE) do
      esc, all, macro = $1, $2, $3.downcase
      args = ($5 || '').split(',').each(&:strip)
      if esc.nil?
        begin
          exec_macro(macro, obj, args)
        rescue => e
          "<div class=\"flash error\">Error executing the <strong>#{macro}</strong> macro (#{e})</div>"
        end || all
      else
        all
      end
    end
  end

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

818 819
  # Renders the TOC with given headings
  def replace_toc(text, headings)
820 821 822 823 824 825 826
    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
827 828 829 830
        out = "<ul class=\"#{div_class}\"><li>"
        root = headings.map(&:first).min
        current = root
        started = false
831
        headings.each do |level, anchor, item|
jplang's avatar
jplang committed
832 833 834 835 836 837 838 839 840 841
          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
842
        end
jplang's avatar
jplang committed
843 844
        out << '</li></ul>' * (current - root)
        out << '</li></ul>'
845 846 847
      end
    end
  end
848

jplang's avatar
jplang committed
849 850 851 852 853
  # 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
854 855
      gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline   -> br
      html_safe
jplang's avatar
jplang committed
856
  end
857

858
  def lang_options_for_select(blank=true)
859
    (blank ? [["(auto)", ""]] : []) +
860
      valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
861
  end
862

863 864 865 866
  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
867

868
  def labelled_tabular_form_for(*args, &proc)
869
    ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_tabular_form_for is deprecated and will be removed in Redmine 1.5. Use #labelled_form_for instead."
870 871
    args << {} unless args.last.is_a?(Hash)
    options = args.last
872
    options[:html] ||= {}
873
    options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
874
    options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
875
    form_for(*args, &proc)
876
  end
877

jplang's avatar
jplang committed
878 879 880
  def labelled_form_for(*args, &proc)
    args << {} unless args.last.is_a?(Hash)
    options = args.last
881
    options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
jplang's avatar
jplang committed
882 883 884
    form_for(*args, &proc)
  end

885 886 887
  def labelled_fields_for(*args, &proc)
    args << {} unless args.last.is_a?(Hash)
    options = args.last
888
    options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
889 890 891 892 893 894
    fields_for(*args, &proc)
  end

  def labelled_remote_form_for(*args, &proc)
    args << {} unless args.last.is_a?(Hash)
    options = args.last
895
    options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
896 897 898
    remote_form_for(*args, &proc)
  end

899
  def back_url_hidden_field_tag
900
    back_url = params[:back_url] || request.env['HTTP_REFERER']
901
    back_url = CGI.unescape(back_url.to_s)
jplang's avatar