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