annotate app/helpers/application_helper.rb @ 45:65d9e2cabaa3 luisf

Added tipoftheday to the config/settings in order to correct previous issues. Tip of the day is now working correctly. Added the heading strings to the locales files.
author luisf
date Tue, 23 Nov 2010 11:50:01 +0000
parents 7f0e922c8982
children 3e75f003034a b859cc0c4fa1 6a2f8e88344e
rev   line source
chris@37 1 # Redmine - project management software
chris@37 2 # Copyright (C) 2006-2010 Jean-Philippe Lang
Chris@0 3 #
Chris@0 4 # This program is free software; you can redistribute it and/or
Chris@0 5 # modify it under the terms of the GNU General Public License
Chris@0 6 # as published by the Free Software Foundation; either version 2
Chris@0 7 # of the License, or (at your option) any later version.
Chris@0 8 #
Chris@0 9 # This program is distributed in the hope that it will be useful,
Chris@0 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
Chris@0 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Chris@0 12 # GNU General Public License for more details.
Chris@0 13 #
Chris@0 14 # You should have received a copy of the GNU General Public License
Chris@0 15 # along with this program; if not, write to the Free Software
Chris@0 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
Chris@0 17
Chris@0 18 require 'forwardable'
Chris@0 19 require 'cgi'
Chris@0 20
Chris@0 21 module ApplicationHelper
Chris@0 22 include Redmine::WikiFormatting::Macros::Definitions
Chris@0 23 include Redmine::I18n
Chris@0 24 include GravatarHelper::PublicMethods
Chris@0 25
Chris@0 26 extend Forwardable
Chris@0 27 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
Chris@0 28
Chris@0 29 # Return true if user is authorized for controller/action, otherwise false
Chris@0 30 def authorize_for(controller, action)
Chris@0 31 User.current.allowed_to?({:controller => controller, :action => action}, @project)
Chris@0 32 end
Chris@0 33
Chris@0 34 # Display a link if user is authorized
chris@22 35 #
chris@22 36 # @param [String] name Anchor text (passed to link_to)
chris@37 37 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
chris@22 38 # @param [optional, Hash] html_options Options passed to link_to
chris@22 39 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
Chris@0 40 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
chris@37 41 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
Chris@0 42 end
Chris@0 43
Chris@0 44 # Display a link to remote if user is authorized
Chris@0 45 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
Chris@0 46 url = options[:url] || {}
Chris@0 47 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
Chris@0 48 end
Chris@0 49
Chris@0 50 # Displays a link to user's account page if active
Chris@0 51 def link_to_user(user, options={})
Chris@0 52 if user.is_a?(User)
Chris@0 53 name = h(user.name(options[:format]))
Chris@0 54 if user.active?
Chris@0 55 link_to name, :controller => 'users', :action => 'show', :id => user
Chris@0 56 else
Chris@0 57 name
Chris@0 58 end
Chris@0 59 else
Chris@0 60 h(user.to_s)
Chris@0 61 end
Chris@0 62 end
Chris@0 63
Chris@0 64 # Displays a link to +issue+ with its subject.
Chris@0 65 # Examples:
Chris@0 66 #
Chris@0 67 # link_to_issue(issue) # => Defect #6: This is the subject
Chris@0 68 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
Chris@0 69 # link_to_issue(issue, :subject => false) # => Defect #6
Chris@0 70 # link_to_issue(issue, :project => true) # => Foo - Defect #6
Chris@0 71 #
Chris@0 72 def link_to_issue(issue, options={})
Chris@0 73 title = nil
Chris@0 74 subject = nil
Chris@0 75 if options[:subject] == false
Chris@0 76 title = truncate(issue.subject, :length => 60)
Chris@0 77 else
Chris@0 78 subject = issue.subject
Chris@0 79 if options[:truncate]
Chris@0 80 subject = truncate(subject, :length => options[:truncate])
Chris@0 81 end
Chris@0 82 end
Chris@0 83 s = link_to "#{issue.tracker} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
Chris@0 84 :class => issue.css_classes,
Chris@0 85 :title => title
Chris@0 86 s << ": #{h subject}" if subject
Chris@0 87 s = "#{h issue.project} - " + s if options[:project]
Chris@0 88 s
Chris@0 89 end
Chris@0 90
Chris@0 91 # Generates a link to an attachment.
Chris@0 92 # Options:
Chris@0 93 # * :text - Link text (default to attachment filename)
Chris@0 94 # * :download - Force download (default: false)
Chris@0 95 def link_to_attachment(attachment, options={})
Chris@0 96 text = options.delete(:text) || attachment.filename
Chris@0 97 action = options.delete(:download) ? 'download' : 'show'
Chris@0 98
Chris@0 99 link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
Chris@0 100 end
Chris@0 101
Chris@0 102 # Generates a link to a SCM revision
Chris@0 103 # Options:
Chris@0 104 # * :text - Link text (default to the formatted revision)
Chris@0 105 def link_to_revision(revision, project, options={})
Chris@0 106 text = options.delete(:text) || format_revision(revision)
Chris@3 107 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
Chris@0 108
Chris@3 109 link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => rev},
Chris@3 110 :title => l(:label_revision_id, format_revision(revision)))
Chris@0 111 end
Chris@0 112
Chris@14 113 # Generates a link to a project if active
Chris@14 114 # Examples:
Chris@14 115 #
Chris@14 116 # link_to_project(project) # => link to the specified project overview
Chris@14 117 # link_to_project(project, :action=>'settings') # => link to project settings
Chris@14 118 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
Chris@14 119 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
Chris@14 120 #
Chris@14 121 def link_to_project(project, options={}, html_options = nil)
Chris@14 122 if project.active?
Chris@14 123 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
Chris@14 124 link_to(h(project), url, html_options)
Chris@14 125 else
Chris@14 126 h(project)
Chris@14 127 end
Chris@14 128 end
Chris@14 129
Chris@0 130 def toggle_link(name, id, options={})
Chris@0 131 onclick = "Element.toggle('#{id}'); "
Chris@0 132 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
Chris@0 133 onclick << "return false;"
Chris@0 134 link_to(name, "#", :onclick => onclick)
Chris@0 135 end
Chris@0 136
Chris@0 137 def image_to_function(name, function, html_options = {})
Chris@0 138 html_options.symbolize_keys!
Chris@0 139 tag(:input, html_options.merge({
Chris@0 140 :type => "image", :src => image_path(name),
Chris@0 141 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
Chris@0 142 }))
Chris@0 143 end
Chris@0 144
Chris@0 145 def prompt_to_remote(name, text, param, url, html_options = {})
Chris@0 146 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
Chris@0 147 link_to name, {}, html_options
Chris@0 148 end
Chris@0 149
Chris@0 150 def format_activity_title(text)
Chris@0 151 h(truncate_single_line(text, :length => 100))
Chris@0 152 end
Chris@0 153
Chris@0 154 def format_activity_day(date)
Chris@0 155 date == Date.today ? l(:label_today).titleize : format_date(date)
Chris@0 156 end
Chris@0 157
Chris@0 158 def format_activity_description(text)
Chris@0 159 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
Chris@0 160 end
Chris@0 161
Chris@0 162 def format_version_name(version)
Chris@0 163 if version.project == @project
Chris@0 164 h(version)
Chris@0 165 else
Chris@0 166 h("#{version.project} - #{version}")
Chris@0 167 end
Chris@0 168 end
Chris@0 169
Chris@0 170 def due_date_distance_in_words(date)
Chris@0 171 if date
Chris@0 172 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
Chris@0 173 end
Chris@0 174 end
Chris@0 175
Chris@0 176 def render_page_hierarchy(pages, node=nil)
Chris@0 177 content = ''
Chris@0 178 if pages[node]
Chris@0 179 content << "<ul class=\"pages-hierarchy\">\n"
Chris@0 180 pages[node].each do |page|
Chris@0 181 content << "<li>"
chris@37 182 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
Chris@0 183 :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
Chris@0 184 content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
Chris@0 185 content << "</li>\n"
Chris@0 186 end
Chris@0 187 content << "</ul>\n"
Chris@0 188 end
Chris@0 189 content
Chris@0 190 end
Chris@0 191
Chris@0 192 # Renders flash messages
Chris@0 193 def render_flash_messages
Chris@0 194 s = ''
Chris@0 195 flash.each do |k,v|
Chris@0 196 s << content_tag('div', v, :class => "flash #{k}")
Chris@0 197 end
Chris@0 198 s
Chris@0 199 end
Chris@0 200
Chris@0 201 # Renders tabs and their content
Chris@0 202 def render_tabs(tabs)
Chris@0 203 if tabs.any?
Chris@0 204 render :partial => 'common/tabs', :locals => {:tabs => tabs}
Chris@0 205 else
Chris@0 206 content_tag 'p', l(:label_no_data), :class => "nodata"
Chris@0 207 end
Chris@0 208 end
Chris@0 209
Chris@0 210 # Renders the project quick-jump box
Chris@0 211 def render_project_jump_box
Chris@0 212 # Retrieve them now to avoid a COUNT query
Chris@0 213 projects = User.current.projects.all
Chris@0 214 if projects.any?
Chris@0 215 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
Chris@0 216 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
Chris@0 217 '<option value="" disabled="disabled">---</option>'
Chris@0 218 s << project_tree_options_for_select(projects, :selected => @project) do |p|
Chris@0 219 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
Chris@0 220 end
Chris@0 221 s << '</select>'
Chris@0 222 s
Chris@0 223 end
Chris@0 224 end
Chris@0 225
Chris@0 226 def project_tree_options_for_select(projects, options = {})
Chris@0 227 s = ''
Chris@0 228 project_tree(projects) do |project, level|
Chris@0 229 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
Chris@0 230 tag_options = {:value => project.id}
Chris@0 231 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
Chris@0 232 tag_options[:selected] = 'selected'
Chris@0 233 else
Chris@0 234 tag_options[:selected] = nil
Chris@0 235 end
Chris@0 236 tag_options.merge!(yield(project)) if block_given?
Chris@0 237 s << content_tag('option', name_prefix + h(project), tag_options)
Chris@0 238 end
Chris@0 239 s
Chris@0 240 end
Chris@0 241
Chris@0 242 # Yields the given block for each project with its level in the tree
chris@37 243 #
chris@37 244 # Wrapper for Project#project_tree
Chris@0 245 def project_tree(projects, &block)
chris@37 246 Project.project_tree(projects, &block)
Chris@0 247 end
Chris@0 248
Chris@0 249 def project_nested_ul(projects, &block)
Chris@0 250 s = ''
Chris@0 251 if projects.any?
Chris@0 252 ancestors = []
Chris@0 253 projects.sort_by(&:lft).each do |project|
Chris@0 254 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
Chris@0 255 s << "<ul>\n"
Chris@0 256 else
Chris@0 257 ancestors.pop
Chris@0 258 s << "</li>"
Chris@0 259 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
Chris@0 260 ancestors.pop
Chris@0 261 s << "</ul></li>\n"
Chris@0 262 end
Chris@0 263 end
Chris@0 264 s << "<li>"
Chris@0 265 s << yield(project).to_s
Chris@0 266 ancestors << project
Chris@0 267 end
Chris@0 268 s << ("</li></ul>\n" * ancestors.size)
Chris@0 269 end
Chris@0 270 s
Chris@0 271 end
Chris@0 272
Chris@0 273 def principals_check_box_tags(name, principals)
Chris@0 274 s = ''
Chris@0 275 principals.sort.each do |principal|
Chris@0 276 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
Chris@0 277 end
Chris@0 278 s
Chris@0 279 end
Chris@0 280
Chris@0 281 # Truncates and returns the string as a single line
Chris@0 282 def truncate_single_line(string, *args)
Chris@0 283 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
Chris@0 284 end
Chris@0 285
Chris@0 286 # Truncates at line break after 250 characters or options[:length]
Chris@0 287 def truncate_lines(string, options={})
Chris@0 288 length = options[:length] || 250
Chris@0 289 if string.to_s =~ /\A(.{#{length}}.*?)$/m
Chris@0 290 "#{$1}..."
Chris@0 291 else
Chris@0 292 string
Chris@0 293 end
Chris@0 294 end
Chris@0 295
Chris@0 296 def html_hours(text)
Chris@0 297 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
Chris@0 298 end
Chris@0 299
Chris@0 300 def authoring(created, author, options={})
Chris@0 301 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created))
Chris@0 302 end
Chris@0 303
Chris@0 304 def time_tag(time)
Chris@0 305 text = distance_of_time_in_words(Time.now, time)
Chris@0 306 if @project
chris@22 307 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
Chris@0 308 else
Chris@0 309 content_tag('acronym', text, :title => format_time(time))
Chris@0 310 end
Chris@0 311 end
Chris@0 312
Chris@0 313 def syntax_highlight(name, content)
Chris@0 314 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
Chris@0 315 end
Chris@0 316
Chris@0 317 def to_path_param(path)
Chris@0 318 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
Chris@0 319 end
Chris@0 320
Chris@0 321 def pagination_links_full(paginator, count=nil, options={})
Chris@0 322 page_param = options.delete(:page_param) || :page
Chris@0 323 per_page_links = options.delete(:per_page_links)
Chris@0 324 url_param = params.dup
Chris@0 325 # don't reuse query params if filters are present
Chris@0 326 url_param.merge!(:fields => nil, :values => nil, :operators => nil) if url_param.delete(:set_filter)
Chris@0 327
Chris@0 328 html = ''
Chris@0 329 if paginator.current.previous
Chris@0 330 html << link_to_remote_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
Chris@0 331 end
Chris@0 332
Chris@0 333 html << (pagination_links_each(paginator, options) do |n|
Chris@0 334 link_to_remote_content_update(n.to_s, url_param.merge(page_param => n))
Chris@0 335 end || '')
Chris@0 336
Chris@0 337 if paginator.current.next
Chris@0 338 html << ' ' + link_to_remote_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next))
Chris@0 339 end
Chris@0 340
Chris@0 341 unless count.nil?
Chris@0 342 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
Chris@0 343 if per_page_links != false && links = per_page_links(paginator.items_per_page)
Chris@0 344 html << " | #{links}"
Chris@0 345 end
Chris@0 346 end
Chris@0 347
Chris@0 348 html
Chris@0 349 end
Chris@0 350
Chris@0 351 def per_page_links(selected=nil)
Chris@0 352 url_param = params.dup
Chris@0 353 url_param.clear if url_param.has_key?(:set_filter)
Chris@0 354
Chris@0 355 links = Setting.per_page_options_array.collect do |n|
Chris@0 356 n == selected ? n : link_to_remote(n, {:update => "content",
Chris@0 357 :url => params.dup.merge(:per_page => n),
Chris@0 358 :method => :get},
Chris@0 359 {:href => url_for(url_param.merge(:per_page => n))})
Chris@0 360 end
Chris@0 361 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
Chris@0 362 end
Chris@0 363
Chris@0 364 def reorder_links(name, url)
Chris@0 365 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), url.merge({"#{name}[move_to]" => 'highest'}), :method => :post, :title => l(:label_sort_highest)) +
Chris@0 366 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), url.merge({"#{name}[move_to]" => 'higher'}), :method => :post, :title => l(:label_sort_higher)) +
Chris@0 367 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), url.merge({"#{name}[move_to]" => 'lower'}), :method => :post, :title => l(:label_sort_lower)) +
Chris@0 368 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), url.merge({"#{name}[move_to]" => 'lowest'}), :method => :post, :title => l(:label_sort_lowest))
Chris@0 369 end
Chris@0 370
Chris@0 371 def breadcrumb(*args)
Chris@0 372 elements = args.flatten
Chris@0 373 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
Chris@0 374 end
Chris@0 375
Chris@0 376 def other_formats_links(&block)
Chris@0 377 concat('<p class="other-formats">' + l(:label_export_to))
Chris@0 378 yield Redmine::Views::OtherFormatsBuilder.new(self)
Chris@0 379 concat('</p>')
Chris@0 380 end
Chris@0 381
Chris@0 382 def page_header_title
Chris@0 383 if @project.nil? || @project.new_record?
Chris@0 384 h(Setting.app_title)
Chris@0 385 else
Chris@0 386 b = []
Chris@0 387 ancestors = (@project.root? ? [] : @project.ancestors.visible)
Chris@0 388 if ancestors.any?
Chris@0 389 root = ancestors.shift
Chris@14 390 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
Chris@0 391 if ancestors.size > 2
Chris@0 392 b << '&#8230;'
Chris@0 393 ancestors = ancestors[-2, 2]
Chris@0 394 end
Chris@14 395 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
Chris@0 396 end
Chris@0 397 b << h(@project)
Chris@0 398 b.join(' &#187; ')
Chris@0 399 end
Chris@0 400 end
Chris@0 401
Chris@0 402 def html_title(*args)
Chris@0 403 if args.empty?
Chris@0 404 title = []
Chris@0 405 title << @project.name if @project
Chris@0 406 title += @html_title if @html_title
Chris@0 407 title << Setting.app_title
Chris@0 408 title.select {|t| !t.blank? }.join(' - ')
Chris@0 409 else
Chris@0 410 @html_title ||= []
Chris@0 411 @html_title += args
Chris@0 412 end
Chris@0 413 end
Chris@0 414
Chris@14 415 # Returns the theme, controller name, and action as css classes for the
Chris@14 416 # HTML body.
Chris@14 417 def body_css_classes
Chris@14 418 css = []
Chris@14 419 if theme = Redmine::Themes.theme(Setting.ui_theme)
Chris@14 420 css << 'theme-' + theme.name
Chris@14 421 end
Chris@14 422
Chris@14 423 css << 'controller-' + params[:controller]
Chris@14 424 css << 'action-' + params[:action]
Chris@14 425 css.join(' ')
Chris@14 426 end
Chris@14 427
Chris@0 428 def accesskey(s)
Chris@0 429 Redmine::AccessKeys.key_for s
Chris@0 430 end
Chris@0 431
Chris@0 432 # Formats text according to system settings.
Chris@0 433 # 2 ways to call this method:
Chris@0 434 # * with a String: textilizable(text, options)
Chris@0 435 # * with an object and one of its attribute: textilizable(issue, :description, options)
Chris@0 436 def textilizable(*args)
Chris@0 437 options = args.last.is_a?(Hash) ? args.pop : {}
Chris@0 438 case args.size
Chris@0 439 when 1
Chris@0 440 obj = options[:object]
Chris@0 441 text = args.shift
Chris@0 442 when 2
Chris@0 443 obj = args.shift
Chris@0 444 attr = args.shift
Chris@0 445 text = obj.send(attr).to_s
Chris@0 446 else
Chris@0 447 raise ArgumentError, 'invalid arguments to textilizable'
Chris@0 448 end
Chris@0 449 return '' if text.blank?
Chris@0 450 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
Chris@0 451 only_path = options.delete(:only_path) == false ? false : true
Chris@0 452
Chris@0 453 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }
Chris@0 454
Chris@0 455 parse_non_pre_blocks(text) do |text|
chris@37 456 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_headings].each do |method_name|
Chris@0 457 send method_name, text, project, obj, attr, only_path, options
Chris@0 458 end
Chris@0 459 end
Chris@0 460 end
Chris@0 461
Chris@0 462 def parse_non_pre_blocks(text)
Chris@0 463 s = StringScanner.new(text)
Chris@0 464 tags = []
Chris@0 465 parsed = ''
Chris@0 466 while !s.eos?
Chris@0 467 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
Chris@0 468 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
Chris@0 469 if tags.empty?
Chris@0 470 yield text
Chris@0 471 end
Chris@0 472 parsed << text
Chris@0 473 if tag
Chris@0 474 if closing
Chris@0 475 if tags.last == tag.downcase
Chris@0 476 tags.pop
Chris@0 477 end
Chris@0 478 else
Chris@0 479 tags << tag.downcase
Chris@0 480 end
Chris@0 481 parsed << full_tag
Chris@0 482 end
Chris@0 483 end
Chris@0 484 # Close any non closing tags
Chris@0 485 while tag = tags.pop
Chris@0 486 parsed << "</#{tag}>"
Chris@0 487 end
Chris@0 488 parsed
Chris@0 489 end
Chris@0 490
Chris@0 491 def parse_inline_attachments(text, project, obj, attr, only_path, options)
Chris@0 492 # when using an image link, try to use an attachment, if possible
Chris@0 493 if options[:attachments] || (obj && obj.respond_to?(:attachments))
Chris@0 494 attachments = nil
Chris@0 495 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
Chris@0 496 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
Chris@0 497 attachments ||= (options[:attachments] || obj.attachments).sort_by(&:created_on).reverse
Chris@0 498 # search for the picture in attachments
Chris@0 499 if found = attachments.detect { |att| att.filename.downcase == filename }
Chris@0 500 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
Chris@0 501 desc = found.description.to_s.gsub('"', '')
Chris@0 502 if !desc.blank? && alttext.blank?
Chris@0 503 alt = " title=\"#{desc}\" alt=\"#{desc}\""
Chris@0 504 end
Chris@0 505 "src=\"#{image_url}\"#{alt}"
Chris@0 506 else
Chris@0 507 m
Chris@0 508 end
Chris@0 509 end
Chris@0 510 end
Chris@0 511 end
Chris@0 512
Chris@0 513 # Wiki links
Chris@0 514 #
Chris@0 515 # Examples:
Chris@0 516 # [[mypage]]
Chris@0 517 # [[mypage|mytext]]
Chris@0 518 # wiki links can refer other project wikis, using project name or identifier:
Chris@0 519 # [[project:]] -> wiki starting page
Chris@0 520 # [[project:|mytext]]
Chris@0 521 # [[project:mypage]]
Chris@0 522 # [[project:mypage|mytext]]
Chris@0 523 def parse_wiki_links(text, project, obj, attr, only_path, options)
Chris@0 524 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
Chris@0 525 link_project = project
Chris@0 526 esc, all, page, title = $1, $2, $3, $5
Chris@0 527 if esc.nil?
Chris@0 528 if page =~ /^([^\:]+)\:(.*)$/
chris@37 529 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
Chris@0 530 page = $2
Chris@0 531 title ||= $1 if page.blank?
Chris@0 532 end
Chris@0 533
Chris@0 534 if link_project && link_project.wiki
Chris@0 535 # extract anchor
Chris@0 536 anchor = nil
Chris@0 537 if page =~ /^(.+?)\#(.+)$/
Chris@0 538 page, anchor = $1, $2
Chris@0 539 end
Chris@0 540 # check if page exists
Chris@0 541 wiki_page = link_project.wiki.find_page(page)
Chris@0 542 url = case options[:wiki_links]
Chris@0 543 when :local; "#{title}.html"
Chris@0 544 when :anchor; "##{title}" # used for single-file wiki export
Chris@0 545 else
chris@37 546 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
chris@37 547 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, :id => wiki_page_id, :anchor => anchor)
Chris@0 548 end
Chris@0 549 link_to((title || page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
Chris@0 550 else
Chris@0 551 # project or wiki doesn't exist
Chris@0 552 all
Chris@0 553 end
Chris@0 554 else
Chris@0 555 all
Chris@0 556 end
Chris@0 557 end
Chris@0 558 end
Chris@0 559
Chris@0 560 # Redmine links
Chris@0 561 #
Chris@0 562 # Examples:
Chris@0 563 # Issues:
Chris@0 564 # #52 -> Link to issue #52
Chris@0 565 # Changesets:
Chris@0 566 # r52 -> Link to revision 52
Chris@0 567 # commit:a85130f -> Link to scmid starting with a85130f
Chris@0 568 # Documents:
Chris@0 569 # document#17 -> Link to document with id 17
Chris@0 570 # document:Greetings -> Link to the document with title "Greetings"
Chris@0 571 # document:"Some document" -> Link to the document with title "Some document"
Chris@0 572 # Versions:
Chris@0 573 # version#3 -> Link to version with id 3
Chris@0 574 # version:1.0.0 -> Link to version named "1.0.0"
Chris@0 575 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
Chris@0 576 # Attachments:
Chris@0 577 # attachment:file.zip -> Link to the attachment of the current object named file.zip
Chris@0 578 # Source files:
Chris@0 579 # source:some/file -> Link to the file located at /some/file in the project's repository
Chris@0 580 # source:some/file@52 -> Link to the file's revision 52
Chris@0 581 # source:some/file#L120 -> Link to line 120 of the file
Chris@0 582 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
Chris@0 583 # export:some/file -> Force the download of the file
Chris@0 584 # Forum messages:
Chris@0 585 # message#1218 -> Link to message with id 1218
Chris@0 586 def parse_redmine_links(text, project, obj, attr, only_path, options)
Chris@0 587 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(attachment|document|version|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m|
Chris@0 588 leading, esc, prefix, sep, identifier = $1, $2, $3, $5 || $7, $6 || $8
Chris@0 589 link = nil
Chris@0 590 if esc.nil?
Chris@0 591 if prefix.nil? && sep == 'r'
Chris@0 592 if project && (changeset = project.changesets.find_by_revision(identifier))
Chris@0 593 link = link_to("r#{identifier}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
Chris@0 594 :class => 'changeset',
Chris@0 595 :title => truncate_single_line(changeset.comments, :length => 100))
Chris@0 596 end
Chris@0 597 elsif sep == '#'
Chris@0 598 oid = identifier.to_i
Chris@0 599 case prefix
Chris@0 600 when nil
Chris@0 601 if issue = Issue.visible.find_by_id(oid, :include => :status)
Chris@0 602 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
Chris@0 603 :class => issue.css_classes,
Chris@0 604 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
Chris@0 605 end
Chris@0 606 when 'document'
Chris@0 607 if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
Chris@0 608 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
Chris@0 609 :class => 'document'
Chris@0 610 end
Chris@0 611 when 'version'
Chris@0 612 if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
Chris@0 613 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
Chris@0 614 :class => 'version'
Chris@0 615 end
Chris@0 616 when 'message'
Chris@0 617 if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
Chris@0 618 link = link_to h(truncate(message.subject, :length => 60)), {:only_path => only_path,
Chris@0 619 :controller => 'messages',
Chris@0 620 :action => 'show',
Chris@0 621 :board_id => message.board,
Chris@0 622 :id => message.root,
Chris@0 623 :anchor => (message.parent ? "message-#{message.id}" : nil)},
Chris@0 624 :class => 'message'
Chris@0 625 end
Chris@0 626 when 'project'
Chris@0 627 if p = Project.visible.find_by_id(oid)
Chris@14 628 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
Chris@0 629 end
Chris@0 630 end
Chris@0 631 elsif sep == ':'
Chris@0 632 # removes the double quotes if any
Chris@0 633 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
Chris@0 634 case prefix
Chris@0 635 when 'document'
Chris@0 636 if project && document = project.documents.find_by_title(name)
Chris@0 637 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
Chris@0 638 :class => 'document'
Chris@0 639 end
Chris@0 640 when 'version'
Chris@0 641 if project && version = project.versions.find_by_name(name)
Chris@0 642 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
Chris@0 643 :class => 'version'
Chris@0 644 end
Chris@0 645 when 'commit'
Chris@0 646 if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
Chris@3 647 link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.identifier},
Chris@0 648 :class => 'changeset',
Chris@0 649 :title => truncate_single_line(changeset.comments, :length => 100)
Chris@0 650 end
Chris@0 651 when 'source', 'export'
Chris@0 652 if project && project.repository
Chris@0 653 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
Chris@0 654 path, rev, anchor = $1, $3, $5
Chris@0 655 link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
Chris@0 656 :path => to_path_param(path),
Chris@0 657 :rev => rev,
Chris@0 658 :anchor => anchor,
Chris@0 659 :format => (prefix == 'export' ? 'raw' : nil)},
Chris@0 660 :class => (prefix == 'export' ? 'source download' : 'source')
Chris@0 661 end
Chris@0 662 when 'attachment'
Chris@0 663 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
Chris@0 664 if attachments && attachment = attachments.detect {|a| a.filename == name }
Chris@0 665 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
Chris@0 666 :class => 'attachment'
Chris@0 667 end
Chris@0 668 when 'project'
Chris@0 669 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
Chris@14 670 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
Chris@0 671 end
Chris@0 672 end
Chris@0 673 end
Chris@0 674 end
Chris@0 675 leading + (link || "#{prefix}#{sep}#{identifier}")
Chris@0 676 end
Chris@0 677 end
chris@37 678
chris@37 679 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
chris@37 680 HEADING_RE = /<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>/i unless const_defined?(:HEADING_RE)
chris@37 681
chris@37 682 # Headings and TOC
chris@37 683 # Adds ids and links to headings and renders the TOC if needed unless options[:headings] is set to false
chris@37 684 def parse_headings(text, project, obj, attr, only_path, options)
chris@37 685 headings = []
chris@37 686 text.gsub!(HEADING_RE) do
chris@37 687 level, attrs, content = $1.to_i, $2, $3
chris@37 688 item = strip_tags(content).strip
chris@37 689 anchor = item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
chris@37 690 headings << [level, anchor, item]
chris@37 691 "<h#{level} #{attrs} id=\"#{anchor}\">#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
chris@37 692 end unless options[:headings] == false
chris@37 693
chris@37 694 text.gsub!(TOC_RE) do
chris@37 695 if headings.empty?
chris@37 696 ''
chris@37 697 else
chris@37 698 div_class = 'toc'
chris@37 699 div_class << ' right' if $1 == '>'
chris@37 700 div_class << ' left' if $1 == '<'
chris@37 701 out = "<ul class=\"#{div_class}\"><li>"
chris@37 702 root = headings.map(&:first).min
chris@37 703 current = root
chris@37 704 started = false
chris@37 705 headings.each do |level, anchor, item|
chris@37 706 if level > current
chris@37 707 out << '<ul><li>' * (level - current)
chris@37 708 elsif level < current
chris@37 709 out << "</li></ul>\n" * (current - level) + "</li><li>"
chris@37 710 elsif started
chris@37 711 out << '</li><li>'
chris@37 712 end
chris@37 713 out << "<a href=\"##{anchor}\">#{item}</a>"
chris@37 714 current = level
chris@37 715 started = true
chris@37 716 end
chris@37 717 out << '</li></ul>' * (current - root)
chris@37 718 out << '</li></ul>'
chris@37 719 end
chris@37 720 end
chris@37 721 end
Chris@0 722
Chris@0 723 # Same as Rails' simple_format helper without using paragraphs
Chris@0 724 def simple_format_without_paragraph(text)
Chris@0 725 text.to_s.
Chris@0 726 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
Chris@0 727 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
Chris@0 728 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
Chris@0 729 end
Chris@0 730
Chris@0 731 def lang_options_for_select(blank=true)
Chris@0 732 (blank ? [["(auto)", ""]] : []) +
Chris@0 733 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
Chris@0 734 end
Chris@0 735
Chris@0 736 def label_tag_for(name, option_tags = nil, options = {})
Chris@0 737 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
Chris@0 738 content_tag("label", label_text)
Chris@0 739 end
Chris@0 740
Chris@0 741 def labelled_tabular_form_for(name, object, options, &proc)
Chris@0 742 options[:html] ||= {}
Chris@0 743 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
Chris@0 744 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
Chris@0 745 end
Chris@0 746
Chris@0 747 def back_url_hidden_field_tag
Chris@0 748 back_url = params[:back_url] || request.env['HTTP_REFERER']
Chris@0 749 back_url = CGI.unescape(back_url.to_s)
Chris@0 750 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
Chris@0 751 end
Chris@0 752
Chris@0 753 def check_all_links(form_name)
Chris@0 754 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
Chris@0 755 " | " +
Chris@0 756 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
Chris@0 757 end
Chris@0 758
Chris@0 759 def progress_bar(pcts, options={})
Chris@0 760 pcts = [pcts, pcts] unless pcts.is_a?(Array)
Chris@0 761 pcts = pcts.collect(&:round)
Chris@0 762 pcts[1] = pcts[1] - pcts[0]
Chris@0 763 pcts << (100 - pcts[1] - pcts[0])
Chris@0 764 width = options[:width] || '100px;'
Chris@0 765 legend = options[:legend] || ''
Chris@0 766 content_tag('table',
Chris@0 767 content_tag('tr',
Chris@0 768 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : '') +
Chris@0 769 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : '') +
Chris@0 770 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : '')
Chris@0 771 ), :class => 'progress', :style => "width: #{width};") +
Chris@0 772 content_tag('p', legend, :class => 'pourcent')
Chris@0 773 end
Chris@0 774
Chris@0 775 def checked_image(checked=true)
Chris@0 776 if checked
Chris@0 777 image_tag 'toggle_check.png'
Chris@0 778 end
Chris@0 779 end
Chris@0 780
Chris@0 781 def context_menu(url)
Chris@0 782 unless @context_menu_included
Chris@0 783 content_for :header_tags do
Chris@0 784 javascript_include_tag('context_menu') +
Chris@0 785 stylesheet_link_tag('context_menu')
Chris@0 786 end
Chris@14 787 if l(:direction) == 'rtl'
Chris@14 788 content_for :header_tags do
Chris@14 789 stylesheet_link_tag('context_menu_rtl')
Chris@14 790 end
Chris@14 791 end
Chris@0 792 @context_menu_included = true
Chris@0 793 end
Chris@0 794 javascript_tag "new ContextMenu('#{ url_for(url) }')"
Chris@0 795 end
Chris@0 796
Chris@0 797 def context_menu_link(name, url, options={})
Chris@0 798 options[:class] ||= ''
Chris@0 799 if options.delete(:selected)
Chris@0 800 options[:class] << ' icon-checked disabled'
Chris@0 801 options[:disabled] = true
Chris@0 802 end
Chris@0 803 if options.delete(:disabled)
Chris@0 804 options.delete(:method)
Chris@0 805 options.delete(:confirm)
Chris@0 806 options.delete(:onclick)
Chris@0 807 options[:class] << ' disabled'
Chris@0 808 url = '#'
Chris@0 809 end
Chris@0 810 link_to name, url, options
Chris@0 811 end
Chris@0 812
Chris@0 813 def calendar_for(field_id)
Chris@0 814 include_calendar_headers_tags
Chris@0 815 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
Chris@0 816 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
Chris@0 817 end
Chris@0 818
Chris@0 819 def include_calendar_headers_tags
Chris@0 820 unless @calendar_headers_tags_included
Chris@0 821 @calendar_headers_tags_included = true
Chris@0 822 content_for :header_tags do
Chris@0 823 start_of_week = case Setting.start_of_week.to_i
Chris@0 824 when 1
Chris@0 825 'Calendar._FD = 1;' # Monday
Chris@0 826 when 7
Chris@0 827 'Calendar._FD = 0;' # Sunday
Chris@0 828 else
Chris@0 829 '' # use language
Chris@0 830 end
Chris@0 831
Chris@0 832 javascript_include_tag('calendar/calendar') +
Chris@0 833 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
Chris@0 834 javascript_tag(start_of_week) +
Chris@0 835 javascript_include_tag('calendar/calendar-setup') +
Chris@0 836 stylesheet_link_tag('calendar')
Chris@0 837 end
Chris@0 838 end
Chris@0 839 end
Chris@0 840
Chris@0 841 def content_for(name, content = nil, &block)
Chris@0 842 @has_content ||= {}
Chris@0 843 @has_content[name] = true
Chris@0 844 super(name, content, &block)
Chris@0 845 end
Chris@0 846
Chris@0 847 def has_content?(name)
Chris@0 848 (@has_content && @has_content[name]) || false
Chris@0 849 end
Chris@0 850
Chris@0 851 # Returns the avatar image tag for the given +user+ if avatars are enabled
Chris@0 852 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
Chris@0 853 def avatar(user, options = { })
Chris@0 854 if Setting.gravatar_enabled?
chris@22 855 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
Chris@0 856 email = nil
Chris@0 857 if user.respond_to?(:mail)
Chris@0 858 email = user.mail
Chris@0 859 elsif user.to_s =~ %r{<(.+?)>}
Chris@0 860 email = $1
Chris@0 861 end
Chris@0 862 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
chris@22 863 else
chris@22 864 ''
Chris@0 865 end
Chris@0 866 end
Chris@0 867
Chris@14 868 def favicon
Chris@14 869 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />"
Chris@14 870 end
Chris@14 871
Chris@0 872 private
Chris@0 873
Chris@0 874 def wiki_helper
Chris@0 875 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
Chris@0 876 extend helper
Chris@0 877 return self
Chris@0 878 end
Chris@0 879
Chris@0 880 def link_to_remote_content_update(text, url_params)
Chris@0 881 link_to_remote(text,
Chris@0 882 {:url => url_params, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'},
Chris@0 883 {:href => url_for(:params => url_params)}
Chris@0 884 )
Chris@0 885 end
Chris@0 886
Chris@0 887 end