annotate app/helpers/application_helper.rb @ 8:0c83d98252d9 yuya

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