Mercurial > hg > soundsoftware-site
diff app/helpers/application_helper.rb @ 909:cbb26bc654de redmine-1.3
Update to Redmine 1.3-stable branch (Redmine SVN rev 8964)
author | Chris Cannam |
---|---|
date | Fri, 24 Feb 2012 19:09:32 +0000 |
parents | cbce1fd3b1b7 |
children | 5e80956cc792 5f33065ddc4b |
line wrap: on
line diff
--- a/app/helpers/application_helper.rb Fri Feb 24 18:36:29 2012 +0000 +++ b/app/helpers/application_helper.rb Fri Feb 24 19:09:32 2012 +0000 @@ -1,3 +1,5 @@ +# encoding: utf-8 +# # Redmine - project management software # Copyright (C) 2006-2011 Jean-Philippe Lang # @@ -80,7 +82,7 @@ subject = truncate(subject, :length => options[:truncate]) end end - s = link_to "#{issue.tracker} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue}, + s = link_to "#{h(issue.tracker)} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue}, :class => issue.css_classes, :title => title s << ": #{h subject}" if subject @@ -95,8 +97,10 @@ def link_to_attachment(attachment, options={}) text = options.delete(:text) || attachment.filename action = options.delete(:download) ? 'download' : 'show' - - link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options) + link_to(h(text), + {:controller => 'attachments', :action => action, + :id => attachment, :filename => attachment.filename }, + options) end # Generates a link to a SCM revision @@ -106,7 +110,7 @@ text = options.delete(:text) || format_revision(revision) rev = revision.respond_to?(:identifier) ? revision.identifier : revision - link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => rev}, + link_to(h(text), {:controller => 'repositories', :action => 'revision', :id => project, :rev => rev}, :title => l(:label_revision_id, format_revision(revision))) end @@ -200,7 +204,7 @@ end content << "</ul>\n" end - content + content.html_safe end # Renders flash messages @@ -209,7 +213,7 @@ flash.each do |k,v| s << content_tag('div', v, :class => "flash #{k}") end - s + s.html_safe end # Renders tabs and their content @@ -233,7 +237,7 @@ { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) } end s << '</select>' - s + s.html_safe end end @@ -250,7 +254,7 @@ tag_options.merge!(yield(project)) if block_given? s << content_tag('option', name_prefix + h(project), tag_options) end - s + s.html_safe end # Yields the given block for each project with its level in the tree @@ -281,7 +285,7 @@ end s << ("</li></ul>\n" * ancestors.size) end - s + s.html_safe end def principals_check_box_tags(name, principals) @@ -289,6 +293,20 @@ principals.sort.each do |principal| s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n" end + s.html_safe + end + + # Returns a string for users/groups option tags + def principals_options_for_select(collection, selected=nil) + s = '' + groups = '' + collection.sort.each do |element| + selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) + (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>) + end + unless groups.empty? + s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>) + end s end @@ -308,11 +326,11 @@ end def html_hours(text) - text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>') + text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe end def authoring(created, author, options={}) - l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)) + l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe end def time_tag(time) @@ -339,7 +357,10 @@ html = '' if paginator.current.previous - html << link_to_content_update('« ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' ' + # \xc2\xab(utf-8) = « + html << link_to_content_update( + "\xc2\xab " + l(:label_previous), + url_param.merge(page_param => paginator.current.previous)) + ' ' end html << (pagination_links_each(paginator, options) do |n| @@ -347,7 +368,10 @@ end || '') if paginator.current.next - html << ' ' + link_to_content_update((l(:label_next) + ' »'), url_param.merge(page_param => paginator.current.next)) + # \xc2\xbb(utf-8) = » + html << ' ' + link_to_content_update( + (l(:label_next) + " \xc2\xbb"), + url_param.merge(page_param => paginator.current.next)) end unless count.nil? @@ -357,7 +381,7 @@ end end - html + html.html_safe end def per_page_links(selected=nil) @@ -367,22 +391,30 @@ links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil end - def reorder_links(name, url) - link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), url.merge({"#{name}[move_to]" => 'highest'}), :method => :post, :title => l(:label_sort_highest)) + - link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), url.merge({"#{name}[move_to]" => 'higher'}), :method => :post, :title => l(:label_sort_higher)) + - link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), url.merge({"#{name}[move_to]" => 'lower'}), :method => :post, :title => l(:label_sort_lower)) + - link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), url.merge({"#{name}[move_to]" => 'lowest'}), :method => :post, :title => l(:label_sort_lowest)) + def reorder_links(name, url, method = :post) + link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), + url.merge({"#{name}[move_to]" => 'highest'}), + :method => method, :title => l(:label_sort_highest)) + + link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), + url.merge({"#{name}[move_to]" => 'higher'}), + :method => method, :title => l(:label_sort_higher)) + + link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), + url.merge({"#{name}[move_to]" => 'lower'}), + :method => method, :title => l(:label_sort_lower)) + + link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), + url.merge({"#{name}[move_to]" => 'lowest'}), + :method => method, :title => l(:label_sort_lowest)) end def breadcrumb(*args) elements = args.flatten - elements.any? ? content_tag('p', args.join(' » ') + ' » ', :class => 'breadcrumb') : nil + elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil end def other_formats_links(&block) - concat('<p class="other-formats">' + l(:label_export_to)) + concat('<p class="other-formats">'.html_safe + l(:label_export_to)) yield Redmine::Views::OtherFormatsBuilder.new(self) - concat('</p>') + concat('</p>'.html_safe) end def page_header_title @@ -395,22 +427,21 @@ root = ancestors.shift b << link_to_project(root, {:jump => current_menu_item}, :class => 'root') if ancestors.size > 2 - b << '…' + b << "\xe2\x80\xa6" ancestors = ancestors[-2, 2] end b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') } end b << h(@project) - b.join(' » ') + b.join(" \xc2\xbb ").html_safe end end def html_title(*args) if args.empty? - title = [] + title = @html_title || [] title << @project.name if @project - title += @html_title if @html_title - title << Setting.app_title + title << Setting.app_title unless Setting.app_title == title.last title.select {|t| !t.blank? }.join(' - ') else @html_title ||= [] @@ -456,11 +487,12 @@ project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil) only_path = options.delete(:only_path) == false ? false : true - text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) } + text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) @parsed_headings = [] + @current_section = 0 if options[:edit_section_links] text = parse_non_pre_blocks(text) do |text| - [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_headings].each do |method_name| + [:parse_sections, :parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_macros, :parse_headings].each do |method_name| send method_name, text, project, obj, attr, only_path, options end end @@ -498,26 +530,26 @@ while tag = tags.pop parsed << "</#{tag}>" end - parsed + parsed.html_safe end def parse_inline_attachments(text, project, obj, attr, only_path, options) # when using an image link, try to use an attachment, if possible if options[:attachments] || (obj && obj.respond_to?(:attachments)) - attachments = nil - text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m| + attachments = options[:attachments] || obj.attachments + text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m| filename, ext, alt, alttext = $1.downcase, $2, $3, $4 - attachments ||= (options[:attachments] || obj.attachments).sort_by(&:created_on).reverse # search for the picture in attachments - if found = attachments.detect { |att| att.filename.downcase == filename } - image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found + if found = Attachment.latest_attach(attachments, filename) + image_url = url_for :only_path => only_path, :controller => 'attachments', + :action => 'download', :id => found desc = found.description.to_s.gsub('"', '') if !desc.blank? && alttext.blank? alt = " title=\"#{desc}\" alt=\"#{desc}\"" end - "src=\"#{image_url}\"#{alt}" + "src=\"#{image_url}\"#{alt}".html_safe else - m + m.html_safe end end end @@ -550,22 +582,27 @@ if page =~ /^(.+?)\#(.+)$/ page, anchor = $1, $2 end + anchor = sanitize_anchor_name(anchor) if anchor.present? # check if page exists wiki_page = link_project.wiki.find_page(page) - url = case options[:wiki_links] - when :local; "#{title}.html" - when :anchor; "##{title}" # used for single-file wiki export + url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page + "##{anchor}" + else + case options[:wiki_links] + when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '') + when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export else wiki_page_id = page.present? ? Wiki.titleize(page) : nil url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, :id => wiki_page_id, :anchor => anchor) end - link_to((title || page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new'))) + end + link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new'))) else # project or wiki doesn't exist - all + all.html_safe end else - all + all.html_safe end end end @@ -603,7 +640,7 @@ # identifier:version:1.0.0 # identifier:source:some/file def parse_redmine_links(text, project, obj, attr, only_path, options) - text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-]+):)?(attachment|document|version|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m| + text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-]+):)?(attachment|document|version|forum|news|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m| leading, esc, project_prefix, project_identifier, prefix, sep, identifier = $1, $2, $3, $4, $5, $7 || $9, $8 || $10 link = nil if project_identifier @@ -613,7 +650,7 @@ if prefix.nil? && sep == 'r' # project.changesets.visible raises an SQL error because of a double join on repositories if project && project.repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(project.repository.id, identifier)) - link = link_to("#{project_prefix}r#{identifier}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision}, + link = link_to(h("#{project_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision}, :class => 'changeset', :title => truncate_single_line(changeset.comments, :length => 100)) end @@ -640,6 +677,16 @@ if message = Message.visible.find_by_id(oid, :include => :parent) link = link_to_message(message, {:only_path => only_path}, :class => 'message') end + when 'forum' + if board = Board.visible.find_by_id(oid) + link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project}, + :class => 'board' + end + when 'news' + if news = News.visible.find_by_id(oid) + link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news}, + :class => 'news' + end when 'project' if p = Project.visible.find_by_id(oid) link = link_to_project(p, {:only_path => only_path}, :class => 'project') @@ -659,11 +706,21 @@ link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version}, :class => 'version' end + when 'forum' + if project && board = project.boards.visible.find_by_name(name) + link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project}, + :class => 'board' + end + when 'news' + if project && news = project.news.visible.find_by_title(name) + link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news}, + :class => 'news' + end when 'commit' if project && project.repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", project.repository.id, "#{name}%"])) link = link_to h("#{project_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.identifier}, :class => 'changeset', - :title => truncate_single_line(changeset.comments, :length => 100) + :title => truncate_single_line(h(changeset.comments), :length => 100) end when 'source', 'export' if project && project.repository && User.current.allowed_to?(:browse_repository, project) @@ -689,11 +746,26 @@ end end end - leading + (link || "#{project_prefix}#{prefix}#{sep}#{identifier}") + (leading + (link || "#{project_prefix}#{prefix}#{sep}#{identifier}")).html_safe end end - HEADING_RE = /<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>/i unless const_defined?(:HEADING_RE) + HEADING_RE = /(<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>)/i unless const_defined?(:HEADING_RE) + + def parse_sections(text, project, obj, attr, only_path, options) + return unless options[:edit_section_links] + text.gsub!(HEADING_RE) do + @current_section += 1 + if @current_section > 1 + content_tag('div', + link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)), + :class => 'contextual', + :title => l(:button_edit_section)) + $1 + else + $1 + end + end + end # Headings and TOC # Adds ids and links to headings unless options[:headings] is set to false @@ -701,14 +773,43 @@ return if options[:headings] == false text.gsub!(HEADING_RE) do - level, attrs, content = $1.to_i, $2, $3 + level, attrs, content = $2.to_i, $3, $4 item = strip_tags(content).strip - anchor = item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-') + anchor = sanitize_anchor_name(item) + # used for single-file wiki export + anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) @parsed_headings << [level, anchor, item] "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">¶</a></h#{level}>" end end + MACROS_RE = / + (!)? # escaping + ( + \{\{ # opening tag + ([\w]+) # macro name + (\(([^\}]*)\))? # optional arguments + \}\} # closing tag + ) + /x unless const_defined?(:MACROS_RE) + + # Macros substitution + def parse_macros(text, project, obj, attr, only_path, options) + text.gsub!(MACROS_RE) do + esc, all, macro = $1, $2, $3.downcase + args = ($5 || '').split(',').each(&:strip) + if esc.nil? + begin + exec_macro(macro, obj, args) + rescue => e + "<div class=\"flash error\">Error executing the <strong>#{macro}</strong> macro (#{e})</div>" + end || all + else + all + end + end + end + TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE) # Renders the TOC with given headings @@ -747,7 +848,8 @@ text.to_s. gsub(/\r\n?/, "\n"). # \r\n and \r -> \n gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br - gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br + gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br + html_safe end def lang_options_for_select(blank=true) @@ -760,10 +862,20 @@ content_tag("label", label_text) end - def labelled_tabular_form_for(name, object, options, &proc) + def labelled_tabular_form_for(*args, &proc) + args << {} unless args.last.is_a?(Hash) + options = args.last options[:html] ||= {} options[:html][:class] = 'tabular' unless options[:html].has_key?(:class) - form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc) + options.merge!({:builder => TabularFormBuilder}) + form_for(*args, &proc) + end + + def labelled_form_for(*args, &proc) + args << {} unless args.last.is_a?(Hash) + options = args.last + options.merge!({:builder => TabularFormBuilder}) + form_for(*args, &proc) end def back_url_hidden_field_tag @@ -774,7 +886,7 @@ def check_all_links(form_name) link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") + - " | " + + " | ".html_safe + link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)") end @@ -787,11 +899,11 @@ legend = options[:legend] || '' content_tag('table', content_tag('tr', - (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : '') + - (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : '') + - (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : '') - ), :class => 'progress', :style => "width: #{width};") + - content_tag('p', legend, :class => 'pourcent') + (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) + + (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) + + (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe) + ), :class => 'progress', :style => "width: #{width};").html_safe + + content_tag('p', legend, :class => 'pourcent').html_safe end def checked_image(checked=true) @@ -829,7 +941,7 @@ options[:class] << ' disabled' url = '#' end - link_to name, url, options + link_to h(name), url, options end def calendar_for(field_id) @@ -872,6 +984,10 @@ (@has_content && @has_content[name]) || false end + def email_delivery_enabled? + !!ActionMailer::Base.perform_deliveries + end + # Returns the avatar image tag for the given +user+ if avatars are enabled # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>') def avatar(user, options = { }) @@ -889,21 +1005,25 @@ end end + def sanitize_anchor_name(anchor) + anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-') + end + # Returns the javascript tags that are included in the html layout head def javascript_heads tags = javascript_include_tag(:defaults) unless User.current.pref.warn_on_leaving_unsaved == '0' - tags << "\n" + javascript_tag("Event.observe(window, 'load', function(){ new WarnLeavingUnsaved('#{escape_javascript( l(:text_warn_on_leaving_unsaved) )}'); });") + tags << "\n".html_safe + javascript_tag("Event.observe(window, 'load', function(){ new WarnLeavingUnsaved('#{escape_javascript( l(:text_warn_on_leaving_unsaved) )}'); });") end tags end def favicon - "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />" + "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe end def robot_exclusion_tag - '<meta name="robots" content="noindex,follow,noarchive" />' + '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe end # Returns true if arg is expected in the API response