application_helper.rb 18.4 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
# redMine - project management software
# Copyright (C) 2006-2007  Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
# 
# 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.
# 
# 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.

module ApplicationHelper
19
  include Redmine::WikiFormatting::Macros::Definitions
20

jplang's avatar
jplang committed
21 22
  def current_role
    @current_role ||= User.current.role_for_project(@project)
23 24 25
  end
  
  # Return true if user is authorized for controller/action, otherwise false
jplang's avatar
jplang committed
26 27
  def authorize_for(controller, action)
    User.current.allowed_to?({:controller => controller, :action => action}, @project)
28 29 30 31
  end

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

  # Display a link to user's account page
  def link_to_user(user)
jplang's avatar
jplang committed
37
    user ? link_to(user, :controller => 'account', :action => 'show', :id => user) : 'Anonymous'
38 39
  end
  
jplang's avatar
jplang committed
40 41 42 43
  def link_to_issue(issue)
    link_to "#{issue.tracker.name} ##{issue.id}", :controller => "issues", :action => "show", :id => issue
  end
  
jplang's avatar
jplang committed
44 45 46 47 48 49 50
  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
  
jplang's avatar
jplang committed
51 52 53
  def show_and_goto_link(name, id, options={})
    onclick = "Element.show('#{id}'); "
    onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
54
    onclick << "Element.scrollTo('#{id}'); "
jplang's avatar
jplang committed
55 56 57 58
    onclick << "return false;"
    link_to(name, "#", options.merge(:onclick => onclick))
  end
  
59 60 61 62 63 64 65 66
  def image_to_function(name, function, html_options = {})
    html_options.symbolize_keys!
    tag(:input, html_options.merge({ 
        :type => "image", :src => image_path(name), 
        :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};" 
        }))
  end
  
67 68 69 70 71
  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
  
72
  def format_date(date)
73
    return nil unless date
74 75
    # "Setting.date_format.size < 2" is a temporary fix (content of date_format setting changed)
    @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
76
    date.strftime(@date_format)
77 78
  end
  
79
  def format_time(time, include_date = true)
80 81
    return nil unless time
    time = time.to_time if time.is_a?(String)
82 83 84 85 86 87
    zone = User.current.time_zone
    if time.utc?
      local = zone ? zone.adjust(time) : time.getlocal
    else
      local = zone ? zone.adjust(time.getutc) : time
    end
88 89
    @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
    @time_format ||= (Setting.time_format.blank? ? l(:general_fmt_time) : Setting.time_format)
90
    include_date ? local.strftime("#{@date_format} #{@time_format}") : local.strftime(@time_format)
91 92
  end
  
93
  def authoring(created, author)
94
    time_tag = content_tag('acronym', distance_of_time_in_words(Time.now, created), :title => format_time(created))
jplang's avatar
jplang committed
95
    l(:label_added_time_by, author || 'Anonymous', time_tag)
96 97
  end
  
98 99 100 101
  def l_or_humanize(s)
    l_has_string?("label_#{s}".to_sym) ? l("label_#{s}".to_sym) : s.to_s.humanize
  end
  
102 103 104 105 106 107 108 109
  def day_name(day)
    l(:general_day_names).split(',')[day-1]
  end
  
  def month_name(month)
    l(:actionview_datehelper_select_month_names).split(',')[month-1]
  end

110
  def pagination_links_full(paginator, count=nil, options={})
111
    page_param = options.delete(:page_param) || :page
112
    url_param = params.dup
113 114
    # don't reuse params if filters are present
    url_param.clear if url_param.has_key?(:set_filter)
115
    
116 117
    html = ''    
    html << link_to_remote(('&#171; ' + l(:label_previous)), 
118 119 120
                            {:update => 'content',
                             :url => url_param.merge(page_param => paginator.current.previous),
                             :complete => 'window.scrollTo(0,0)'},
121
                            {:href => url_for(:params => url_param.merge(page_param => paginator.current.previous))}) + ' ' if paginator.current.previous
122 123 124
                            
    html << (pagination_links_each(paginator, options) do |n|
      link_to_remote(n.to_s, 
125 126 127
                      {:url => {:params => url_param.merge(page_param => n)},
                       :update => 'content',
                       :complete => 'window.scrollTo(0,0)'},
128
                      {:href => url_for(:params => url_param.merge(page_param => n))})
129 130 131
    end || '')
    
    html << ' ' + link_to_remote((l(:label_next) + ' &#187;'), 
132 133 134
                                 {:update => 'content',
                                  :url => url_param.merge(page_param => paginator.current.next),
                                  :complete => 'window.scrollTo(0,0)'},
135 136 137 138 139 140
                                 {:href => url_for(:params => url_param.merge(page_param => paginator.current.next))}) if paginator.current.next
    
    unless count.nil?
      html << [" (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})", per_page_links(paginator.items_per_page)].compact.join(' | ')
    end
    
141 142 143
    html  
  end
  
144
  def per_page_links(selected=nil)
145 146 147
    url_param = params.dup
    url_param.clear if url_param.has_key?(:set_filter)
    
148 149
    links = Setting.per_page_options_array.collect do |n|
      n == selected ? n : link_to_remote(n, {:update => "content", :url => params.dup.merge(:per_page => n)}, 
150
                                            {:href => url_for(url_param.merge(:per_page => n))})
151 152 153 154
    end
    links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
  end
  
155 156 157 158 159 160 161 162 163 164 165
  def html_title(*args)
    if args.empty?
      title = []
      title << @project.name if @project
      title += @html_title if @html_title
      title << Setting.app_title
      title.compact.join(' - ')
    else
      @html_title ||= []
      @html_title += args
    end
166
  end
jplang's avatar
jplang committed
167 168

  def accesskey(s)
169
    Redmine::AccessKeys.key_for s
jplang's avatar
jplang committed
170 171
  end

172 173 174 175 176 177 178 179 180
  # 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
      obj = nil
jplang's avatar
jplang committed
181
      text = args.shift
182 183
    when 2
      obj = args.shift
jplang's avatar
jplang committed
184
      text = obj.send(args.shift).to_s
185 186 187
    else
      raise ArgumentError, 'invalid arguments to textilizable'
    end
jplang's avatar
jplang committed
188 189 190
    return '' if text.blank?
    
    only_path = options.delete(:only_path) == false ? false : true
191 192

    # when using an image link, try to use an attachment, if possible
jplang's avatar
jplang committed
193 194
    attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
    
195
    if attachments
196 197 198
      text = text.gsub(/!((\<|\=|\>)?(\([^\)]+\))?(\[[^\]]+\])?(\{[^\}]+\})?)(\S+\.(gif|jpg|jpeg|png))!/) do |m|
        style = $1
        filename = $6
199 200 201
        rf = Regexp.new(filename,  Regexp::IGNORECASE)
        # search for the picture in attachments
        if found = attachments.detect { |att| att.filename =~ rf }
jplang's avatar
jplang committed
202
          image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found.id
203
          "!#{style}#{image_url}!"
204
        else
205
          "!#{style}#{filename}!"
206 207 208
        end
      end
    end
209
    
210
    text = (Setting.text_formatting == 'textile') ?
211 212
      Redmine::WikiFormatting.to_html(text) { |macro, args| exec_macro(macro, obj, args) } :
      simple_format(auto_link(h(text)))
213

214 215 216 217
    # different methods for formatting wiki links
    case options[:wiki_links]
    when :local
      # used for local links to html files
218
      format_wiki_link = Proc.new {|project, title| "#{title}.html" }
219 220
    when :anchor
      # used for single-file wiki export
221
      format_wiki_link = Proc.new {|project, title| "##{title}" }
222
    else
jplang's avatar
jplang committed
223
      format_wiki_link = Proc.new {|project, title| url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => project, :page => title) }
224 225
    end
    
jplang's avatar
jplang committed
226
    project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
227
    
228 229 230
    # Wiki links
    # 
    # Examples:
231 232 233 234
    #   [[mypage]]
    #   [[mypage|mytext]]
    # wiki links can refer other project wikis, using project name or identifier:
    #   [[project:]] -> wiki starting page
235
    #   [[project:|mytext]]
236 237
    #   [[project:mypage]]
    #   [[project:mypage|mytext]]
238
    text = text.gsub(/(!)?(\[\[([^\]\|]+)(\|([^\]\|]+))?\]\])/) do |m|
239
      link_project = project
240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256
      esc, all, page, title = $1, $2, $3, $5
      if esc.nil?
        if page =~ /^([^\:]+)\:(.*)$/
          link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
          page = $2
          title ||= $1 if page.blank?
        end
        
        if link_project && link_project.wiki
          # check if page exists
          wiki_page = link_project.wiki.find_page(page)
          link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page)),
                                   :class => ('wiki-page' + (wiki_page ? '' : ' new')))
        else
          # project or wiki doesn't exist
          title || page
        end
jplang's avatar
jplang committed
257
      else
258
        all
jplang's avatar
jplang committed
259
      end
260
    end
261

262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280
    # Redmine links
    # 
    # Examples:
    #   Issues:
    #     #52 -> Link to issue #52
    #   Changesets:
    #     r52 -> Link to revision 52
    #   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
    text = text.gsub(%r{([\s\(,-^])(!)?(attachment|document|version)?((#|r)(\d+)|(:)([^"][^\s<>]+|"[^"]+"))(?=[[:punct:]]|\s|<|$)}) do |m|
      leading, esc, prefix, sep, oid = $1, $2, $3, $5 || $7, $6 || $8
281
      link = nil
282 283 284
      if esc.nil?
        if prefix.nil? && sep == 'r'
          if project && (changeset = project.changesets.find_by_revision(oid))
jplang's avatar
jplang committed
285 286
            link = link_to("r#{oid}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project.id, :rev => oid},
                                      :class => 'changeset',
287 288 289 290 291 292 293
                                      :title => truncate(changeset.comments, 100))
          end
        elsif sep == '#'
          oid = oid.to_i
          case prefix
          when nil
            if issue = Issue.find_by_id(oid, :include => [:project, :status], :conditions => Project.visible_by(User.current))        
jplang's avatar
jplang committed
294 295
              link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
                                        :class => 'issue',
296 297 298 299 300
                                        :title => "#{truncate(issue.subject, 100)} (#{issue.status.name})")
              link = content_tag('del', link) if issue.closed?
            end
          when 'document'
            if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
jplang's avatar
jplang committed
301 302
              link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
                                                :class => 'document'
303 304 305
            end
          when 'version'
            if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
jplang's avatar
jplang committed
306 307
              link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
                                              :class => 'version'
308 309 310 311 312 313 314 315
            end
          end
        elsif sep == ':'
          # removes the double quotes if any
          name = oid.gsub(%r{^"(.*)"$}, "\\1")
          case prefix
          when 'document'
            if project && document = project.documents.find_by_title(name)
jplang's avatar
jplang committed
316 317
              link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
                                                :class => 'document'
318 319 320
            end
          when 'version'
            if project && version = project.versions.find_by_name(name)
jplang's avatar
jplang committed
321 322
              link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
                                              :class => 'version'
323 324 325
            end
          when 'attachment'
            if attachments && attachment = attachments.detect {|a| a.filename == name }
jplang's avatar
jplang committed
326 327
              link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
                                                     :class => 'attachment'
328 329
            end
          end
jplang's avatar
jplang committed
330
        end
331
      end
332
      leading + (link || "#{prefix}#{sep}#{oid}")
333
    end
334 335
    
    text
336 337
  end
  
jplang's avatar
jplang committed
338 339 340 341 342 343 344 345
  # Same as Rails' simple_format helper without using paragraphs
  def simple_format_without_paragraph(text)
    text.to_s.
      gsub(/\r\n?/, "\n").                    # \r\n and \r -> \n
      gsub(/\n\n+/, "<br /><br />").          # 2+ newline  -> 2 br
      gsub(/([^\n]\n)(?=[^\n])/, '\1<br />')  # 1 newline   -> br
  end
  
346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372
  def error_messages_for(object_name, options = {})
    options = options.symbolize_keys
    object = instance_variable_get("@#{object_name}")
    if object && !object.errors.empty?
      # build full_messages here with controller current language
      full_messages = []
      object.errors.each do |attr, msg|
        next if msg.nil?
        msg = msg.first if msg.is_a? Array
        if attr == "base"
          full_messages << l(msg)
        else
          full_messages << "&#171; " + (l_has_string?("field_" + attr) ? l("field_" + attr) : object.class.human_attribute_name(attr)) + " &#187; " + l(msg) unless attr == "custom_values"
        end
      end
      # retrieve custom values error messages
      if object.errors[:custom_values]
        object.custom_values.each do |v| 
          v.errors.each do |attr, msg|
            next if msg.nil?
            msg = msg.first if msg.is_a? Array
            full_messages << "&#171; " + v.custom_field.name + " &#187; " + l(msg)
          end
        end
      end      
      content_tag("div",
        content_tag(
373
          options[:header_tag] || "span", lwr(:gui_validation_error, full_messages.length) + ":"
374 375 376 377 378 379 380 381 382 383 384
        ) +
        content_tag("ul", full_messages.collect { |msg| content_tag("li", msg) }),
        "id" => options[:id] || "errorExplanation", "class" => options[:class] || "errorExplanation"
      )
    else
      ""
    end
  end
  
  def lang_options_for_select(blank=true)
    (blank ? [["(auto)", ""]] : []) + 
385
      GLoc.valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404
  end
  
  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
  
  def labelled_tabular_form_for(name, object, options, &proc)
    options[:html] ||= {}
    options[:html].store :class, "tabular"
    form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
  end
  
  def check_all_links(form_name)
    link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
    " | " +
    link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")   
  end
  
405 406 407 408
  def progress_bar(pcts, options={})
    pcts = [pcts, pcts] unless pcts.is_a?(Array)
    pcts[1] = pcts[1] - pcts[0]
    pcts << (100 - pcts[1] - pcts[0])
409 410 411 412
    width = options[:width] || '100px;'
    legend = options[:legend] || ''
    content_tag('table',
      content_tag('tr',
413 414 415
        (pcts[0] > 0 ? content_tag('td', '', :width => "#{pcts[0].floor}%;", :class => 'closed') : '') +
        (pcts[1] > 0 ? content_tag('td', '', :width => "#{pcts[1].floor}%;", :class => 'done') : '') +
        (pcts[2] > 0 ? content_tag('td', '', :width => "#{pcts[2].floor}%;", :class => 'todo') : '')
416 417 418 419
      ), :class => 'progress', :style => "width: #{width};") +
      content_tag('p', legend, :class => 'pourcent')
  end
  
420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435
  def context_menu_link(name, url, options={})
    options[:class] ||= ''
    if options.delete(:selected)
      options[:class] << ' icon-checked disabled'
      options[:disabled] = true
    end
    if options.delete(:disabled)
      options.delete(:method)
      options.delete(:confirm)
      options.delete(:onclick)
      options[:class] << ' disabled'
      url = '#'
    end
    link_to name, url, options
  end
  
436 437 438 439
  def calendar_for(field_id)
    image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
    javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
  end
jplang's avatar
jplang committed
440 441 442
  
  def wikitoolbar_for(field_id)
    return '' unless Setting.text_formatting == 'textile'
443 444 445
    javascript_include_tag('jstoolbar/jstoolbar') +
      javascript_include_tag("jstoolbar/lang/jstoolbar-#{current_language}") +
      javascript_tag("var toolbar = new jsToolBar($('#{field_id}')); toolbar.draw();")
jplang's avatar
jplang committed
446
  end
jplang's avatar
jplang committed
447 448 449 450 451 452 453 454 455 456
  
  def content_for(name, content = nil, &block)
    @has_content ||= {}
    @has_content[name] = true
    super(name, content, &block)
  end
  
  def has_content?(name)
    (@has_content && @has_content[name]) || false
  end
jplang's avatar
jplang committed
457
end