To check out this repository please hg clone the following URL, or open the URL using EasyMercurial or your preferred Mercurial client.

Statistics Download as Zip
| Branch: | Tag: | Revision:

root / app / helpers / application_helper.rb @ 1541:2696466256ff

History | View | Annotate | Download (50.1 KB)

1
# encoding: utf-8
2
#
3
# Redmine - project management software
4
# Copyright (C) 2006-2014  Jean-Philippe Lang
5
#
6
# This program is free software; you can redistribute it and/or
7
# modify it under the terms of the GNU General Public License
8
# as published by the Free Software Foundation; either version 2
9
# of the License, or (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19

    
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
  include Redmine::Pagination::Helper
28

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

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

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

    
47
  # Displays a link to user's account page if active
48
  def link_to_user(user, options={})
49
    if user.is_a?(User)
50
      name = h(user.name(options[:format]))
51
      if user.active? || (User.current.admin? && user.logged?)
52
        link_to name, user_path(user), :class => user.css_classes
53
      else
54
        name
55
      end
56
    else
57
      h(user.to_s)
58
    end
59
  end
60

    
61
  # Displays a link to +issue+ with its subject.
62
  # Examples:
63
  #
64
  #   link_to_issue(issue)                        # => Defect #6: This is the subject
65
  #   link_to_issue(issue, :truncate => 6)        # => Defect #6: This i...
66
  #   link_to_issue(issue, :subject => false)     # => Defect #6
67
  #   link_to_issue(issue, :project => true)      # => Foo - Defect #6
68
  #   link_to_issue(issue, :subject => false, :tracker => false)     # => #6
69
  #
70
  def link_to_issue(issue, options={})
71
    title = nil
72
    subject = nil
73
    text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
74
    if options[:subject] == false
75
      title = issue.subject.truncate(60)
76
    else
77
      subject = issue.subject
78
      if truncate_length = options[:truncate]
79
        subject = subject.truncate(truncate_length)
80
      end
81
    end
82
    only_path = options[:only_path].nil? ? true : options[:only_path]
83
    s = link_to(text, issue_path(issue, :only_path => only_path),
84
                :class => issue.css_classes, :title => title)
85
    s << h(": #{subject}") if subject
86
    s = h("#{issue.project} - ") + s if options[:project]
87
    s
88
  end
89

    
90
  # Generates a link to an attachment.
91
  # Options:
92
  # * :text - Link text (default to attachment filename)
93
  # * :download - Force download (default: false)
94
  def link_to_attachment(attachment, options={})
95
    text = options.delete(:text) || attachment.filename
96
    route_method = options.delete(:download) ? :download_named_attachment_path : :named_attachment_path
97
    html_options = options.slice!(:only_path)
98
    url = send(route_method, attachment, attachment.filename, options)
99
    link_to text, url, html_options
100
  end
101

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

    
118
  # Generates a link to a message
119
  def link_to_message(message, options={}, html_options = nil)
120
    link_to(
121
      message.subject.truncate(60),
122
      board_message_path(message.board_id, message.parent_id || message.id, {
123
        :r => (message.parent_id && message.id),
124
        :anchor => (message.parent_id ? "message-#{message.id}" : nil)
125
      }.merge(options)),
126
      html_options
127
    )
128
  end
129

    
130
  # Generates a link to a project if active
131
  # Examples:
132
  #
133
  #   link_to_project(project)                          # => link to the specified project overview
134
  #   link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
135
  #   link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
136
  #
137
  def link_to_project(project, options={}, html_options = nil)
138
    if project.archived?
139
      h(project.name)
140
    elsif options.key?(:action)
141
      ActiveSupport::Deprecation.warn "#link_to_project with :action option is deprecated and will be removed in Redmine 3.0."
142
      url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
143
      link_to project.name, url, html_options
144
    else
145
      link_to project.name, project_path(project, options), html_options
146
    end
147
  end
148

    
149
  # Generates a link to a project settings if active
150
  def link_to_project_settings(project, options={}, html_options=nil)
151
    if project.active?
152
      link_to project.name, settings_project_path(project, options), html_options
153
    elsif project.archived?
154
      h(project.name)
155
    else
156
      link_to project.name, project_path(project, options), html_options
157
    end
158
  end
159

    
160
  # Helper that formats object for html or text rendering
161
  def format_object(object, html=true)
162
    case object.class.name
163
    when 'Array'
164
      object.map {|o| format_object(o, html)}.join(', ').html_safe
165
    when 'Time'
166
      format_time(object)
167
    when 'Date'
168
      format_date(object)
169
    when 'Fixnum'
170
      object.to_s
171
    when 'Float'
172
      sprintf "%.2f", object
173
    when 'User'
174
      html ? link_to_user(object) : object.to_s
175
    when 'Project'
176
      html ? link_to_project(object) : object.to_s
177
    when 'Version'
178
      html ? link_to(object.name, version_path(object)) : object.to_s
179
    when 'TrueClass'
180
      l(:general_text_Yes)
181
    when 'FalseClass'
182
      l(:general_text_No)
183
    when 'Issue'
184
      object.visible? && html ? link_to_issue(object) : "##{object.id}"
185
    when 'CustomValue', 'CustomFieldValue'
186
      if object.custom_field
187
        f = object.custom_field.format.formatted_custom_value(self, object, html)
188
        if f.nil? || f.is_a?(String)
189
          f
190
        else
191
          format_object(f, html)
192
        end
193
      else
194
        object.value.to_s
195
      end
196
    else
197
      html ? h(object) : object.to_s
198
    end
199
  end
200

    
201
  def wiki_page_path(page, options={})
202
    url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
203
  end
204

    
205
  def thumbnail_tag(attachment)
206
    link_to image_tag(thumbnail_path(attachment)),
207
      named_attachment_path(attachment, attachment.filename),
208
      :title => attachment.filename
209
  end
210

    
211
  def toggle_link(name, id, options={})
212
    onclick = "$('##{id}').toggle(); "
213
    onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
214
    onclick << "return false;"
215
    link_to(name, "#", :onclick => onclick)
216
  end
217

    
218
  def image_to_function(name, function, html_options = {})
219
    html_options.symbolize_keys!
220
    tag(:input, html_options.merge({
221
        :type => "image", :src => image_path(name),
222
        :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
223
        }))
224
  end
225

    
226
  def format_activity_title(text)
227
    h(truncate_single_line_raw(text, 100))
228
  end
229

    
230
  def format_activity_day(date)
231
    date == User.current.today ? l(:label_today).titleize : format_date(date)
232
  end
233

    
234
  def format_activity_description(text)
235
    h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
236
       ).gsub(/[\r\n]+/, "<br />").html_safe
237
  end
238

    
239
  def format_version_name(version)
240
    if version.project == @project
241
      h(version)
242
    else
243
      h("#{version.project} - #{version}")
244
    end
245
  end
246

    
247
  def due_date_distance_in_words(date)
248
    if date
249
      l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
250
    end
251
  end
252

    
253
  # Renders a tree of projects as a nested set of unordered lists
254
  # The given collection may be a subset of the whole project tree
255
  # (eg. some intermediate nodes are private and can not be seen)
256
  def render_project_nested_lists(projects)
257
    s = ''
258
    if projects.any?
259
      ancestors = []
260
      original_project = @project
261
      projects.sort_by(&:lft).each do |project|
262
        # set the project environment to please macros.
263
        @project = project
264
        if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
265
          s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
266
        else
267
          ancestors.pop
268
          s << "</li>"
269
          while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
270
            ancestors.pop
271
            s << "</ul></li>\n"
272
          end
273
        end
274
        classes = (ancestors.empty? ? 'root' : 'child')
275
        s << "<li class='#{classes}'><div class='#{classes}'>"
276
        s << h(block_given? ? yield(project) : project.name)
277
        s << "</div>\n"
278
        ancestors << project
279
      end
280
      s << ("</li></ul>\n" * ancestors.size)
281
      @project = original_project
282
    end
283
    s.html_safe
284
  end
285

    
286
  def render_page_hierarchy(pages, node=nil, options={})
287
    content = ''
288
    if pages[node]
289
      content << "<ul class=\"pages-hierarchy\">\n"
290
      pages[node].each do |page|
291
        content << "<li>"
292
        content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
293
                           :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
294
        content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
295
        content << "</li>\n"
296
      end
297
      content << "</ul>\n"
298
    end
299
    content.html_safe
300
  end
301

    
302
  # Renders flash messages
303
  def render_flash_messages
304
    s = ''
305
    flash.each do |k,v|
306
      s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
307
    end
308
    s.html_safe
309
  end
310

    
311
  # Renders tabs and their content
312
  def render_tabs(tabs, selected=params[:tab])
313
    if tabs.any?
314
      unless tabs.detect {|tab| tab[:name] == selected}
315
        selected = nil
316
      end
317
      selected ||= tabs.first[:name]
318
      render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
319
    else
320
      content_tag 'p', l(:label_no_data), :class => "nodata"
321
    end
322
  end
323

    
324
  # Renders the project quick-jump box
325
  def render_project_jump_box
326
    return unless User.current.logged?
327
    projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
328
    if projects.any?
329
      options =
330
        ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
331
         '<option value="" disabled="disabled">---</option>').html_safe
332

    
333
      options << project_tree_options_for_select(projects, :selected => @project) do |p|
334
        { :value => project_path(:id => p, :jump => current_menu_item) }
335
      end
336

    
337
      select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
338
    end
339
  end
340

    
341
  def project_tree_options_for_select(projects, options = {})
342
    s = ''
343
    project_tree(projects) do |project, level|
344
      name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
345
      tag_options = {:value => project.id}
346
      if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
347
        tag_options[:selected] = 'selected'
348
      else
349
        tag_options[:selected] = nil
350
      end
351
      tag_options.merge!(yield(project)) if block_given?
352
      s << content_tag('option', name_prefix + h(project), tag_options)
353
    end
354
    s.html_safe
355
  end
356

    
357
  # Yields the given block for each project with its level in the tree
358
  #
359
  # Wrapper for Project#project_tree
360
  def project_tree(projects, &block)
361
    Project.project_tree(projects, &block)
362
  end
363

    
364
  def principals_check_box_tags(name, principals)
365
    s = ''
366

    
367
    principals.sort.each do |principal|
368

    
369
      if principal.type == "User"
370
        s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{link_to_user principal}</label>\n"
371
      else
372
        s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal} (Group)</label>\n"
373
      end
374

    
375
    end
376
    s.html_safe
377
  end
378

    
379
  # Returns a string for users/groups option tags
380
  def principals_options_for_select(collection, selected=nil)
381
    s = ''
382
    if collection.include?(User.current)
383
      s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
384
    end
385
    groups = ''
386
    collection.sort.each do |element|
387
      selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
388
      (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
389
    end
390
    unless groups.empty?
391
      s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
392
    end
393
    s.html_safe
394
  end
395

    
396
  # Options for the new membership projects combo-box
397
  def options_for_membership_project_select(principal, projects)
398
    options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
399
    options << project_tree_options_for_select(projects) do |p|
400
      {:disabled => principal.projects.to_a.include?(p)}
401
    end
402
    options
403
  end
404

    
405
  def option_tag(name, text, value, selected=nil, options={})
406
    content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
407
  end
408

    
409
  # Truncates and returns the string as a single line
410
  def truncate_single_line(string, *args)
411
    ActiveSupport::Deprecation.warn(
412
      "ApplicationHelper#truncate_single_line is deprecated and will be removed in Rails 4 poring")
413
    # Rails 4 ActionView::Helpers::TextHelper#truncate escapes.
414
    # So, result is broken.
415
    truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
416
  end
417

    
418
  def truncate_single_line_raw(string, length)
419
    string.truncate(length).gsub(%r{[\r\n]+}m, ' ')
420
  end
421

    
422
  # Truncates at line break after 250 characters or options[:length]
423
  def truncate_lines(string, options={})
424
    length = options[:length] || 250
425
    if string.to_s =~ /\A(.{#{length}}.*?)$/m
426
      "#{$1}..."
427
    else
428
      string
429
    end
430
  end
431

    
432
  def anchor(text)
433
    text.to_s.gsub(' ', '_')
434
  end
435

    
436
  def html_hours(text)
437
    text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
438
  end
439

    
440
  def authoring(created, author, options={})
441
    l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
442
  end
443

    
444
  def time_tag(time)
445
    text = distance_of_time_in_words(Time.now, time)
446
    if @project
447
      link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
448
    else
449
      content_tag('abbr', text, :title => format_time(time))
450
    end
451
  end
452

    
453
  def syntax_highlight_lines(name, content)
454
    lines = []
455
    syntax_highlight(name, content).each_line { |line| lines << line }
456
    lines
457
  end
458

    
459
  def syntax_highlight(name, content)
460
    Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
461
  end
462

    
463
  def to_path_param(path)
464
    str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
465
    str.blank? ? nil : str
466
  end
467

    
468
  def reorder_links(name, url, method = :post)
469
    link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
470
            url.merge({"#{name}[move_to]" => 'highest'}),
471
            :method => method, :title => l(:label_sort_highest)) +
472
    link_to(image_tag('1uparrow.png',   :alt => l(:label_sort_higher)),
473
            url.merge({"#{name}[move_to]" => 'higher'}),
474
           :method => method, :title => l(:label_sort_higher)) +
475
    link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
476
            url.merge({"#{name}[move_to]" => 'lower'}),
477
            :method => method, :title => l(:label_sort_lower)) +
478
    link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
479
            url.merge({"#{name}[move_to]" => 'lowest'}),
480
           :method => method, :title => l(:label_sort_lowest))
481
  end
482

    
483
  def breadcrumb(*args)
484
    elements = args.flatten
485
    elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
486
  end
487

    
488
  def other_formats_links(&block)
489
    concat('<p class="other-formats">'.html_safe + l(:label_export_to))
490
    yield Redmine::Views::OtherFormatsBuilder.new(self)
491
    concat('</p>'.html_safe)
492
  end
493

    
494
  def page_header_title
495
    if @project.nil? || @project.new_record?
496
      a = [h(Setting.app_title), '']
497

    
498
    else
499
      pname = []
500
      b = []
501
      ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
502
      if ancestors.any?
503
        root = ancestors.shift
504
        b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
505
        if ancestors.size > 2
506
          b << '&#8230;'
507
          ancestors = ancestors[-2, 2]
508
        end
509
        b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
510
        b = b.join(' &#187; ').html_safe
511
        b << (' &#187;'.html_safe)
512
      end
513

    
514
      pname << h(@project)
515

    
516
      a = [pname, b]
517

    
518
    end
519
  end
520

    
521
  # Returns a h2 tag and sets the html title with the given arguments
522
  def title(*args)
523
    strings = args.map do |arg|
524
      if arg.is_a?(Array) && arg.size >= 2
525
        link_to(*arg)
526
      else
527
        h(arg.to_s)
528
      end
529
    end
530
    html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
531
    content_tag('h2', strings.join(' &#187; ').html_safe)
532
  end
533

    
534
  # Sets the html title
535
  # Returns the html title when called without arguments
536
  # Current project name and app_title and automatically appended
537
  # Exemples:
538
  #   html_title 'Foo', 'Bar'
539
  #   html_title # => 'Foo - Bar - My Project - Redmine'
540
  def html_title(*args)
541
    if args.empty?
542
      title = @html_title || []
543
      title << @project.name if @project
544
      title << Setting.app_title unless Setting.app_title == title.last
545
      title.reject(&:blank?).join(' - ')
546
    else
547
      @html_title ||= []
548
      @html_title += args
549
    end
550
  end
551

    
552
  # Returns the theme, controller name, and action as css classes for the
553
  # HTML body.
554
  def body_css_classes
555
    css = []
556
    if theme = Redmine::Themes.theme(Setting.ui_theme)
557
      css << 'theme-' + theme.name
558
    end
559

    
560
    css << 'project-' + @project.identifier if @project && @project.identifier.present?
561
    css << 'controller-' + controller_name
562
    css << 'action-' + action_name
563
    css.join(' ')
564
  end
565

    
566
  def accesskey(s)
567
    @used_accesskeys ||= []
568
    key = Redmine::AccessKeys.key_for(s)
569
    return nil if @used_accesskeys.include?(key)
570
    @used_accesskeys << key
571
    key
572
  end
573

    
574
  # Formats text according to system settings.
575
  # 2 ways to call this method:
576
  # * with a String: textilizable(text, options)
577
  # * with an object and one of its attribute: textilizable(issue, :description, options)
578
  def textilizable(*args)
579
    options = args.last.is_a?(Hash) ? args.pop : {}
580
    case args.size
581
    when 1
582
      obj = options[:object]
583
      text = args.shift
584
    when 2
585
      obj = args.shift
586
      attr = args.shift
587
      text = obj.send(attr).to_s
588
    else
589
      raise ArgumentError, 'invalid arguments to textilizable'
590
    end
591
    return '' if text.blank?
592
    project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
593
    only_path = options.delete(:only_path) == false ? false : true
594

    
595
    text = text.dup
596
    macros = catch_macros(text)
597
    text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
598

    
599
    @parsed_headings = []
600
    @heading_anchors = {}
601
    @current_section = 0 if options[:edit_section_links]
602

    
603
    parse_sections(text, project, obj, attr, only_path, options)
604
    text = parse_non_pre_blocks(text, obj, macros) do |text|
605
      [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
606
        send method_name, text, project, obj, attr, only_path, options
607
      end
608
    end
609
    parse_headings(text, project, obj, attr, only_path, options)
610

    
611
    if @parsed_headings.any?
612
      replace_toc(text, @parsed_headings)
613
    end
614

    
615
    text.html_safe
616
  end
617

    
618
  def parse_non_pre_blocks(text, obj, macros)
619
    s = StringScanner.new(text)
620
    tags = []
621
    parsed = ''
622
    while !s.eos?
623
      s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
624
      text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
625
      if tags.empty?
626
        yield text
627
        inject_macros(text, obj, macros) if macros.any?
628
      else
629
        inject_macros(text, obj, macros, false) if macros.any?
630
      end
631
      parsed << text
632
      if tag
633
        if closing
634
          if tags.last == tag.downcase
635
            tags.pop
636
          end
637
        else
638
          tags << tag.downcase
639
        end
640
        parsed << full_tag
641
      end
642
    end
643
    # Close any non closing tags
644
    while tag = tags.pop
645
      parsed << "</#{tag}>"
646
    end
647
    parsed
648
  end
649

    
650
  def parse_inline_attachments(text, project, obj, attr, only_path, options)
651
    # when using an image link, try to use an attachment, if possible
652
    attachments = options[:attachments] || []
653
    attachments += obj.attachments if obj.respond_to?(:attachments)
654
    if attachments.present?
655
      text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
656
        filename, ext, alt, alttext = $1.downcase, $2, $3, $4
657
        # search for the picture in attachments
658
        if found = Attachment.latest_attach(attachments, filename)
659
          image_url = download_named_attachment_path(found, found.filename, :only_path => only_path)
660
          desc = found.description.to_s.gsub('"', '')
661
          if !desc.blank? && alttext.blank?
662
            alt = " title=\"#{desc}\" alt=\"#{desc}\""
663
          end
664
          "src=\"#{image_url}\"#{alt}"
665
        else
666
          m
667
        end
668
      end
669
    end
670
  end
671

    
672
  # Wiki links
673
  #
674
  # Examples:
675
  #   [[mypage]]
676
  #   [[mypage|mytext]]
677
  # wiki links can refer other project wikis, using project name or identifier:
678
  #   [[project:]] -> wiki starting page
679
  #   [[project:|mytext]]
680
  #   [[project:mypage]]
681
  #   [[project:mypage|mytext]]
682
  def parse_wiki_links(text, project, obj, attr, only_path, options)
683
    text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
684
      link_project = project
685
      esc, all, page, title = $1, $2, $3, $5
686
      if esc.nil?
687
        if page =~ /^([^\:]+)\:(.*)$/
688
          identifier, page = $1, $2
689
          link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
690
          title ||= identifier if page.blank?
691
        end
692

    
693
        if link_project && link_project.wiki
694
          # extract anchor
695
          anchor = nil
696
          if page =~ /^(.+?)\#(.+)$/
697
            page, anchor = $1, $2
698
          end
699
          anchor = sanitize_anchor_name(anchor) if anchor.present?
700
          # check if page exists
701
          wiki_page = link_project.wiki.find_page(page)
702
          url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
703
            "##{anchor}"
704
          else
705
            case options[:wiki_links]
706
            when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
707
            when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
708
            else
709
              wiki_page_id = page.present? ? Wiki.titleize(page) : nil
710
              parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
711
              url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
712
               :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
713
            end
714
          end
715
          link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
716
        else
717
          # project or wiki doesn't exist
718
          all
719
        end
720
      else
721
        all
722
      end
723
    end
724
  end
725

    
726
  # Redmine links
727
  #
728
  # Examples:
729
  #   Issues:
730
  #     #52 -> Link to issue #52
731
  #   Changesets:
732
  #     r52 -> Link to revision 52
733
  #     commit:a85130f -> Link to scmid starting with a85130f
734
  #   Documents:
735
  #     document#17 -> Link to document with id 17
736
  #     document:Greetings -> Link to the document with title "Greetings"
737
  #     document:"Some document" -> Link to the document with title "Some document"
738
  #   Versions:
739
  #     version#3 -> Link to version with id 3
740
  #     version:1.0.0 -> Link to version named "1.0.0"
741
  #     version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
742
  #   Attachments:
743
  #     attachment:file.zip -> Link to the attachment of the current object named file.zip
744
  #   Source files:
745
  #     source:some/file -> Link to the file located at /some/file in the project's repository
746
  #     source:some/file@52 -> Link to the file's revision 52
747
  #     source:some/file#L120 -> Link to line 120 of the file
748
  #     source:some/file@52#L120 -> Link to line 120 of the file's revision 52
749
  #     export:some/file -> Force the download of the file
750
  #   Forum messages:
751
  #     message#1218 -> Link to message with id 1218
752
  #  Projects:
753
  #     project:someproject -> Link to project named "someproject"
754
  #     project#3 -> Link to project with id 3
755
  #
756
  #   Links can refer other objects from other projects, using project identifier:
757
  #     identifier:r52
758
  #     identifier:document:"Some document"
759
  #     identifier:version:1.0.0
760
  #     identifier:source:some/file
761
  def parse_redmine_links(text, default_project, obj, attr, only_path, options)
762
    text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-_]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
763
      leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $2, $3, $4, $5, $10, $11, $8 || $12 || $18, $14 || $19, $15, $17
764
      link = nil
765
      project = default_project
766
      if project_identifier
767
        project = Project.visible.find_by_identifier(project_identifier)
768
      end
769
      if esc.nil?
770
        if prefix.nil? && sep == 'r'
771
          if project
772
            repository = nil
773
            if repo_identifier
774
              repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
775
            else
776
              repository = project.repository
777
            end
778
            # project.changesets.visible raises an SQL error because of a double join on repositories
779
            if repository &&
780
                 (changeset = Changeset.visible.
781
                                  find_by_repository_id_and_revision(repository.id, identifier))
782
              link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
783
                             {:only_path => only_path, :controller => 'repositories',
784
                              :action => 'revision', :id => project,
785
                              :repository_id => repository.identifier_param,
786
                              :rev => changeset.revision},
787
                             :class => 'changeset',
788
                             :title => truncate_single_line_raw(changeset.comments, 100))
789
            end
790
          end
791
        elsif sep == '#'
792
          oid = identifier.to_i
793
          case prefix
794
          when nil
795
            if oid.to_s == identifier &&
796
                  issue = Issue.visible.includes(:status).find_by_id(oid)
797
              anchor = comment_id ? "note-#{comment_id}" : nil
798
              link = link_to(h("##{oid}#{comment_suffix}"),
799
                             {:only_path => only_path, :controller => 'issues',
800
                              :action => 'show', :id => oid, :anchor => anchor},
801
                             :class => issue.css_classes,
802
                             :title => "#{issue.subject.truncate(100)} (#{issue.status.name})")
803
            end
804
          when 'document'
805
            if document = Document.visible.find_by_id(oid)
806
              link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
807
                                                :class => 'document'
808
            end
809
          when 'version'
810
            if version = Version.visible.find_by_id(oid)
811
              link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
812
                                              :class => 'version'
813
            end
814
          when 'message'
815
            if message = Message.visible.includes(:parent).find_by_id(oid)
816
              link = link_to_message(message, {:only_path => only_path}, :class => 'message')
817
            end
818
          when 'forum'
819
            if board = Board.visible.find_by_id(oid)
820
              link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
821
                                             :class => 'board'
822
            end
823
          when 'news'
824
            if news = News.visible.find_by_id(oid)
825
              link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
826
                                            :class => 'news'
827
            end
828
          when 'project'
829
            if p = Project.visible.find_by_id(oid)
830
              link = link_to_project(p, {:only_path => only_path}, :class => 'project')
831
            end
832
          end
833
        elsif sep == ':'
834
          # removes the double quotes if any
835
          name = identifier.gsub(%r{^"(.*)"$}, "\\1")
836
          name = CGI.unescapeHTML(name)
837
          case prefix
838
          when 'document'
839
            if project && document = project.documents.visible.find_by_title(name)
840
              link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
841
                                                :class => 'document'
842
            end
843
          when 'version'
844
            if project && version = project.versions.visible.find_by_name(name)
845
              link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
846
                                              :class => 'version'
847
            end
848
          when 'forum'
849
            if project && board = project.boards.visible.find_by_name(name)
850
              link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
851
                                             :class => 'board'
852
            end
853
          when 'news'
854
            if project && news = project.news.visible.find_by_title(name)
855
              link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
856
                                            :class => 'news'
857
            end
858
          when 'commit', 'source', 'export'
859
            if project
860
              repository = nil
861
              if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
862
                repo_prefix, repo_identifier, name = $1, $2, $3
863
                repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
864
              else
865
                repository = project.repository
866
              end
867
              if prefix == 'commit'
868
                if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
869
                  link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
870
                                               :class => 'changeset',
871
                                               :title => truncate_single_line_raw(changeset.comments, 100)
872
                end
873
              else
874
                if repository && User.current.allowed_to?(:browse_repository, project)
875
                  name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
876
                  path, rev, anchor = $1, $3, $5
877
                  link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
878
                                                          :path => to_path_param(path),
879
                                                          :rev => rev,
880
                                                          :anchor => anchor},
881
                                                         :class => (prefix == 'export' ? 'source download' : 'source')
882
                end
883
              end
884
              repo_prefix = nil
885
            end
886
          when 'attachment'
887
            attachments = options[:attachments] || []
888
            attachments += obj.attachments if obj.respond_to?(:attachments)
889
            if attachments && attachment = Attachment.latest_attach(attachments, name)
890
              link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
891
            end
892
          when 'project'
893
            if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
894
              link = link_to_project(p, {:only_path => only_path}, :class => 'project')
895
            end
896
          end
897
        end
898
      end
899
      (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
900
    end
901
  end
902

    
903
  HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
904

    
905
  def parse_sections(text, project, obj, attr, only_path, options)
906
    return unless options[:edit_section_links]
907
    text.gsub!(HEADING_RE) do
908
      heading = $1
909
      @current_section += 1
910
      if @current_section > 1
911
        content_tag('div',
912
          link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
913
          :class => 'contextual',
914
          :title => l(:button_edit_section),
915
          :id => "section-#{@current_section}") + heading.html_safe
916
      else
917
        heading
918
      end
919
    end
920
  end
921

    
922
  # Headings and TOC
923
  # Adds ids and links to headings unless options[:headings] is set to false
924
  def parse_headings(text, project, obj, attr, only_path, options)
925
    return if options[:headings] == false
926

    
927
    text.gsub!(HEADING_RE) do
928
      level, attrs, content = $2.to_i, $3, $4
929
      item = strip_tags(content).strip
930
      anchor = sanitize_anchor_name(item)
931
      # used for single-file wiki export
932
      anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
933
      @heading_anchors[anchor] ||= 0
934
      idx = (@heading_anchors[anchor] += 1)
935
      if idx > 1
936
        anchor = "#{anchor}-#{idx}"
937
      end
938
      @parsed_headings << [level, anchor, item]
939
      "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
940
    end
941
  end
942

    
943
  MACROS_RE = /(
944
                (!)?                        # escaping
945
                (
946
                \{\{                        # opening tag
947
                ([\w]+)                     # macro name
948
                (\(([^\n\r]*?)\))?          # optional arguments
949
                ([\n\r].*?[\n\r])?          # optional block of text
950
                \}\}                        # closing tag
951
                )
952
               )/mx unless const_defined?(:MACROS_RE)
953

    
954
  MACRO_SUB_RE = /(
955
                  \{\{
956
                  macro\((\d+)\)
957
                  \}\}
958
                  )/x unless const_defined?(:MACRO_SUB_RE)
959

    
960
  # Extracts macros from text
961
  def catch_macros(text)
962
    macros = {}
963
    text.gsub!(MACROS_RE) do
964
      all, macro = $1, $4.downcase
965
      if macro_exists?(macro) || all =~ MACRO_SUB_RE
966
        index = macros.size
967
        macros[index] = all
968
        "{{macro(#{index})}}"
969
      else
970
        all
971
      end
972
    end
973
    macros
974
  end
975

    
976
  # Executes and replaces macros in text
977
  def inject_macros(text, obj, macros, execute=true)
978
    text.gsub!(MACRO_SUB_RE) do
979
      all, index = $1, $2.to_i
980
      orig = macros.delete(index)
981
      if execute && orig && orig =~ MACROS_RE
982
        esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
983
        if esc.nil?
984
          h(exec_macro(macro, obj, args, block) || all)
985
        else
986
          h(all)
987
        end
988
      elsif orig
989
        h(orig)
990
      else
991
        h(all)
992
      end
993
    end
994
  end
995

    
996
  TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
997

    
998
  # Renders the TOC with given headings
999
  def replace_toc(text, headings)
1000
    text.gsub!(TOC_RE) do
1001
      left_align, right_align = $2, $3
1002
      # Keep only the 4 first levels
1003
      headings = headings.select{|level, anchor, item| level <= 4}
1004
      if headings.empty?
1005
        ''
1006
      else
1007
        div_class = 'toc'
1008
        div_class << ' right' if right_align
1009
        div_class << ' left' if left_align
1010
        out = "<ul class=\"#{div_class}\"><li>"
1011
        root = headings.map(&:first).min
1012
        current = root
1013
        started = false
1014
        headings.each do |level, anchor, item|
1015
          if level > current
1016
            out << '<ul><li>' * (level - current)
1017
          elsif level < current
1018
            out << "</li></ul>\n" * (current - level) + "</li><li>"
1019
          elsif started
1020
            out << '</li><li>'
1021
          end
1022
          out << "<a href=\"##{anchor}\">#{item}</a>"
1023
          current = level
1024
          started = true
1025
        end
1026
        out << '</li></ul>' * (current - root)
1027
        out << '</li></ul>'
1028
      end
1029
    end
1030
  end
1031

    
1032
  # Same as Rails' simple_format helper without using paragraphs
1033
  def simple_format_without_paragraph(text)
1034
    text.to_s.
1035
      gsub(/\r\n?/, "\n").                    # \r\n and \r -> \n
1036
      gsub(/\n\n+/, "<br /><br />").          # 2+ newline  -> 2 br
1037
      gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline   -> br
1038
      html_safe
1039
  end
1040

    
1041
  def lang_options_for_select(blank=true)
1042
    (blank ? [["(auto)", ""]] : []) + languages_options
1043
  end
1044

    
1045
  def label_tag_for(name, option_tags = nil, options = {})
1046
    label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
1047
    content_tag("label", label_text)
1048
  end
1049

    
1050
  def labelled_form_for(*args, &proc)
1051
    args << {} unless args.last.is_a?(Hash)
1052
    options = args.last
1053
    if args.first.is_a?(Symbol)
1054
      options.merge!(:as => args.shift)
1055
    end
1056
    options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1057
    form_for(*args, &proc)
1058
  end
1059

    
1060
  def labelled_fields_for(*args, &proc)
1061
    args << {} unless args.last.is_a?(Hash)
1062
    options = args.last
1063
    options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1064
    fields_for(*args, &proc)
1065
  end
1066

    
1067
  def labelled_remote_form_for(*args, &proc)
1068
    ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_remote_form_for is deprecated and will be removed in Redmine 2.2."
1069
    args << {} unless args.last.is_a?(Hash)
1070
    options = args.last
1071
    options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
1072
    form_for(*args, &proc)
1073
  end
1074

    
1075
  def error_messages_for(*objects)
1076
    html = ""
1077
    objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1078
    errors = objects.map {|o| o.errors.full_messages}.flatten
1079
    if errors.any?
1080
      html << "<div id='errorExplanation'><ul>\n"
1081
      errors.each do |error|
1082
        html << "<li>#{h error}</li>\n"
1083
      end
1084
      html << "</ul></div>\n"
1085
    end
1086
    html.html_safe
1087
  end
1088

    
1089
  def delete_link(url, options={})
1090
    options = {
1091
      :method => :delete,
1092
      :data => {:confirm => l(:text_are_you_sure)},
1093
      :class => 'icon icon-del'
1094
    }.merge(options)
1095

    
1096
    link_to l(:button_delete), url, options
1097
  end
1098

    
1099
  def preview_link(url, form, target='preview', options={})
1100
    content_tag 'a', l(:label_preview), {
1101
        :href => "#",
1102
        :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1103
        :accesskey => accesskey(:preview)
1104
      }.merge(options)
1105
  end
1106

    
1107
  def link_to_function(name, function, html_options={})
1108
    content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1109
  end
1110

    
1111
  # Helper to render JSON in views
1112
  def raw_json(arg)
1113
    arg.to_json.to_s.gsub('/', '\/').html_safe
1114
  end
1115

    
1116
  def back_url
1117
    url = params[:back_url]
1118
    if url.nil? && referer = request.env['HTTP_REFERER']
1119
      url = CGI.unescape(referer.to_s)
1120
    end
1121
    url
1122
  end
1123

    
1124
  def back_url_hidden_field_tag
1125
    url = back_url
1126
    hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1127
  end
1128

    
1129
  def check_all_links(form_name)
1130
    link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1131
    " | ".html_safe +
1132
    link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1133
  end
1134

    
1135
  def progress_bar(pcts, options={})
1136
    pcts = [pcts, pcts] unless pcts.is_a?(Array)
1137
    pcts = pcts.collect(&:round)
1138
    pcts[1] = pcts[1] - pcts[0]
1139
    pcts << (100 - pcts[1] - pcts[0])
1140
    width = options[:width] || '100px;'
1141
    legend = options[:legend] || ''
1142
    content_tag('table',
1143
      content_tag('tr',
1144
        (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1145
        (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1146
        (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1147
      ), :class => "progress progress-#{pcts[0]}", :style => "width: #{width};").html_safe +
1148
      content_tag('p', legend, :class => 'percent').html_safe
1149
  end
1150

    
1151
  def checked_image(checked=true)
1152
    if checked
1153
      image_tag 'toggle_check.png'
1154
    end
1155
  end
1156

    
1157
  def context_menu(url)
1158
    unless @context_menu_included
1159
      content_for :header_tags do
1160
        javascript_include_tag('context_menu') +
1161
          stylesheet_link_tag('context_menu')
1162
      end
1163
      if l(:direction) == 'rtl'
1164
        content_for :header_tags do
1165
          stylesheet_link_tag('context_menu_rtl')
1166
        end
1167
      end
1168
      @context_menu_included = true
1169
    end
1170
    javascript_tag "contextMenuInit('#{ url_for(url) }')"
1171
  end
1172

    
1173
  def calendar_for(field_id)
1174
    include_calendar_headers_tags
1175
    javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1176
  end
1177

    
1178
  def include_calendar_headers_tags
1179
    unless @calendar_headers_tags_included
1180
      tags = javascript_include_tag("datepicker")
1181
      @calendar_headers_tags_included = true
1182
      content_for :header_tags do
1183
        start_of_week = Setting.start_of_week
1184
        start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1185
        # Redmine uses 1..7 (monday..sunday) in settings and locales
1186
        # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1187
        start_of_week = start_of_week.to_i % 7
1188
        tags << javascript_tag(
1189
                   "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1190
                     "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1191
                     path_to_image('/images/calendar.png') +
1192
                     "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1193
                     "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1194
                     "beforeShow: beforeShowDatePicker};")
1195
        jquery_locale = l('jquery.locale', :default => current_language.to_s)
1196
        unless jquery_locale == 'en'
1197
          tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js")
1198
        end
1199
        tags
1200
      end
1201
    end
1202
  end
1203

    
1204
  # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1205
  # Examples:
1206
  #   stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1207
  #   stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1208
  #
1209
  def stylesheet_link_tag(*sources)
1210
    options = sources.last.is_a?(Hash) ? sources.pop : {}
1211
    plugin = options.delete(:plugin)
1212
    sources = sources.map do |source|
1213
      if plugin
1214
        "/plugin_assets/#{plugin}/stylesheets/#{source}"
1215
      elsif current_theme && current_theme.stylesheets.include?(source)
1216
        current_theme.stylesheet_path(source)
1217
      else
1218
        source
1219
      end
1220
    end
1221
    super sources, options
1222
  end
1223

    
1224
  # Overrides Rails' image_tag with themes and plugins support.
1225
  # Examples:
1226
  #   image_tag('image.png') # => picks image.png from the current theme or defaults
1227
  #   image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1228
  #
1229
  def image_tag(source, options={})
1230
    if plugin = options.delete(:plugin)
1231
      source = "/plugin_assets/#{plugin}/images/#{source}"
1232
    elsif current_theme && current_theme.images.include?(source)
1233
      source = current_theme.image_path(source)
1234
    end
1235
    super source, options
1236
  end
1237

    
1238
  # Overrides Rails' javascript_include_tag with plugins support
1239
  # Examples:
1240
  #   javascript_include_tag('scripts') # => picks scripts.js from defaults
1241
  #   javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1242
  #
1243
  def javascript_include_tag(*sources)
1244
    options = sources.last.is_a?(Hash) ? sources.pop : {}
1245
    if plugin = options.delete(:plugin)
1246
      sources = sources.map do |source|
1247
        if plugin
1248
          "/plugin_assets/#{plugin}/javascripts/#{source}"
1249
        else
1250
          source
1251
        end
1252
      end
1253
    end
1254
    super sources, options
1255
  end
1256

    
1257
  # TODO: remove this in 2.5.0
1258
  def has_content?(name)
1259
    content_for?(name)
1260
  end
1261

    
1262
  def sidebar_content?
1263
    content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1264
  end
1265

    
1266
  def view_layouts_base_sidebar_hook_response
1267
    @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1268
  end
1269

    
1270
  def email_delivery_enabled?
1271
    !!ActionMailer::Base.perform_deliveries
1272
  end
1273

    
1274
  # Returns the avatar image tag for the given +user+ if avatars are enabled
1275
  # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1276
  def avatar(user, options = { })
1277
    if Setting.gravatar_enabled?
1278
      options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1279
      email = nil
1280
      if user.respond_to?(:mail)
1281
        email = user.mail
1282
      elsif user.to_s =~ %r{<(.+?)>}
1283
        email = $1
1284
      end
1285
      return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1286
    else
1287
      ''
1288
    end
1289
  end
1290

    
1291
  def sanitize_anchor_name(anchor)
1292
    if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java'
1293
      anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1294
    else
1295
      # TODO: remove when ruby1.8 is no longer supported
1296
      anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1297
    end
1298
  end
1299

    
1300
  # Returns the javascript tags that are included in the html layout head
1301
  def javascript_heads
1302
    tags = javascript_include_tag('jquery-1.8.3-ui-1.9.2-ujs-2.0.3', 'application')
1303
    unless User.current.pref.warn_on_leaving_unsaved == '0'
1304
      tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1305
    end
1306
    tags
1307
  end
1308

    
1309
  def favicon
1310
    "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1311
  end
1312

    
1313
  # Returns the path to the favicon
1314
  def favicon_path
1315
    icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1316
    image_path(icon)
1317
  end
1318

    
1319
  # Returns the full URL to the favicon
1320
  def favicon_url
1321
    # TODO: use #image_url introduced in Rails4
1322
    path = favicon_path
1323
    base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1324
    base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1325
  end
1326

    
1327
  def robot_exclusion_tag
1328
    '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1329
  end
1330

    
1331
  def stylesheet_platform_font_tag
1332
    agent = request.env['HTTP_USER_AGENT']
1333
    name = 'fonts-generic'
1334
    if agent and agent =~ %r{Windows}
1335
      name = 'fonts-ms'
1336
    elsif agent and agent =~ %r{Macintosh}
1337
      name = 'fonts-mac'
1338
    end
1339
    stylesheet_link_tag name, :media => 'all'
1340
  end
1341

    
1342
  # Returns true if arg is expected in the API response
1343
  def include_in_api_response?(arg)
1344
    unless @included_in_api_response
1345
      param = params[:include]
1346
      @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1347
      @included_in_api_response.collect!(&:strip)
1348
    end
1349
    @included_in_api_response.include?(arg.to_s)
1350
  end
1351

    
1352
  # Returns options or nil if nometa param or X-Redmine-Nometa header
1353
  # was set in the request
1354
  def api_meta(options)
1355
    if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1356
      # compatibility mode for activeresource clients that raise
1357
      # an error when unserializing an array with attributes
1358
      nil
1359
    else
1360
      options
1361
    end
1362
  end
1363

    
1364
  private
1365

    
1366
  def wiki_helper
1367
    helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1368
    extend helper
1369
    return self
1370
  end
1371

    
1372
  def link_to_content_update(text, url_params = {}, html_options = {})
1373
    link_to(text, url_params, html_options)
1374
  end
1375
end