application_helper.rb 40.2 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
              wiki_page_id = page.present? ? Wiki.titleize(page) : nil
599 600 601
              parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
              url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, 
               :id => wiki_page_id, :anchor => anchor, :parent => parent)
602
            end
603
          end
emassip's avatar
emassip committed
604
          link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
605 606
        else
          # project or wiki doesn't exist
607
          all.html_safe
608
        end
jplang's avatar
jplang committed
609
      else
610
        all.html_safe
jplang's avatar
jplang committed
611
      end
612
    end
613
  end
614

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

758 759 760 761 762
  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
763 764
      @current_section += 1
      if @current_section > 1
765
        content_tag('div',
766
          link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
767 768 769 770 771 772 773
          :class => 'contextual',
          :title => l(:button_edit_section)) + $1
      else
        $1
      end
    end
  end
774

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

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

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

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

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

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

865 866 867 868
  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
869

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

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

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

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

901
  def back_url_hidden_field_tag