To check out this repository please hg clone the following URL, or open the URL using EasyMercurial or your preferred Mercurial client.
root / .svn / pristine / 01 / 012acdb93702d840e809c52a4f350ace647a5f34.svn-base @ 1297:0a574315af3e
History | View | Annotate | Download (47.4 KB)
| 1 | 1296:038ba2d95de8 | Chris | # encoding: utf-8 |
|---|---|---|---|
| 2 | # |
||
| 3 | # Redmine - project management software |
||
| 4 | # Copyright (C) 2006-2012 Jean-Philippe Lang |
||
| 5 | # |
||
| 6 | # This program is free software; you can redistribute it and/or |
||
| 7 | # modify it under the terms of the GNU General Public License |
||
| 8 | # as published by the Free Software Foundation; either version 2 |
||
| 9 | # of the License, or (at your option) any later version. |
||
| 10 | # |
||
| 11 | # This program is distributed in the hope that it will be useful, |
||
| 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 14 | # GNU General Public License for more details. |
||
| 15 | # |
||
| 16 | # You should have received a copy of the GNU General Public License |
||
| 17 | # along with this program; if not, write to the Free Software |
||
| 18 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 19 | |||
| 20 | require 'forwardable' |
||
| 21 | require 'cgi' |
||
| 22 | |||
| 23 | module ApplicationHelper |
||
| 24 | include Redmine::WikiFormatting::Macros::Definitions |
||
| 25 | include Redmine::I18n |
||
| 26 | include GravatarHelper::PublicMethods |
||
| 27 | |||
| 28 | extend Forwardable |
||
| 29 | def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter |
||
| 30 | |||
| 31 | # Return true if user is authorized for controller/action, otherwise false |
||
| 32 | def authorize_for(controller, action) |
||
| 33 | User.current.allowed_to?({:controller => controller, :action => action}, @project)
|
||
| 34 | end |
||
| 35 | |||
| 36 | # Display a link if user is authorized |
||
| 37 | # |
||
| 38 | # @param [String] name Anchor text (passed to link_to) |
||
| 39 | # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized |
||
| 40 | # @param [optional, Hash] html_options Options passed to link_to |
||
| 41 | # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to |
||
| 42 | def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
|
||
| 43 | link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action]) |
||
| 44 | end |
||
| 45 | |||
| 46 | # Displays a link to user's account page if active |
||
| 47 | def link_to_user(user, options={})
|
||
| 48 | if user.is_a?(User) |
||
| 49 | name = h(user.name(options[:format])) |
||
| 50 | if user.active? || (User.current.admin? && user.logged?) |
||
| 51 | link_to name, user_path(user), :class => user.css_classes |
||
| 52 | else |
||
| 53 | name |
||
| 54 | end |
||
| 55 | else |
||
| 56 | h(user.to_s) |
||
| 57 | end |
||
| 58 | end |
||
| 59 | |||
| 60 | # Displays a link to +issue+ with its subject. |
||
| 61 | # Examples: |
||
| 62 | # |
||
| 63 | # link_to_issue(issue) # => Defect #6: This is the subject |
||
| 64 | # link_to_issue(issue, :truncate => 6) # => Defect #6: This i... |
||
| 65 | # link_to_issue(issue, :subject => false) # => Defect #6 |
||
| 66 | # link_to_issue(issue, :project => true) # => Foo - Defect #6 |
||
| 67 | # link_to_issue(issue, :subject => false, :tracker => false) # => #6 |
||
| 68 | # |
||
| 69 | def link_to_issue(issue, options={})
|
||
| 70 | title = nil |
||
| 71 | subject = nil |
||
| 72 | text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
|
||
| 73 | if options[:subject] == false |
||
| 74 | title = truncate(issue.subject, :length => 60) |
||
| 75 | else |
||
| 76 | subject = issue.subject |
||
| 77 | if options[:truncate] |
||
| 78 | subject = truncate(subject, :length => options[:truncate]) |
||
| 79 | end |
||
| 80 | end |
||
| 81 | s = link_to text, issue_path(issue), :class => issue.css_classes, :title => title |
||
| 82 | s << h(": #{subject}") if subject
|
||
| 83 | s = h("#{issue.project} - ") + s if options[:project]
|
||
| 84 | s |
||
| 85 | end |
||
| 86 | |||
| 87 | # Generates a link to an attachment. |
||
| 88 | # Options: |
||
| 89 | # * :text - Link text (default to attachment filename) |
||
| 90 | # * :download - Force download (default: false) |
||
| 91 | def link_to_attachment(attachment, options={})
|
||
| 92 | text = options.delete(:text) || attachment.filename |
||
| 93 | action = options.delete(:download) ? 'download' : 'show' |
||
| 94 | opt_only_path = {}
|
||
| 95 | opt_only_path[:only_path] = (options[:only_path] == false ? false : true) |
||
| 96 | options.delete(:only_path) |
||
| 97 | link_to(h(text), |
||
| 98 | {:controller => 'attachments', :action => action,
|
||
| 99 | :id => attachment, :filename => attachment.filename}.merge(opt_only_path), |
||
| 100 | options) |
||
| 101 | end |
||
| 102 | |||
| 103 | # Generates a link to a SCM revision |
||
| 104 | # Options: |
||
| 105 | # * :text - Link text (default to the formatted revision) |
||
| 106 | def link_to_revision(revision, repository, options={})
|
||
| 107 | if repository.is_a?(Project) |
||
| 108 | repository = repository.repository |
||
| 109 | end |
||
| 110 | text = options.delete(:text) || format_revision(revision) |
||
| 111 | rev = revision.respond_to?(:identifier) ? revision.identifier : revision |
||
| 112 | link_to( |
||
| 113 | h(text), |
||
| 114 | {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
|
||
| 115 | :title => l(:label_revision_id, format_revision(revision)) |
||
| 116 | ) |
||
| 117 | end |
||
| 118 | |||
| 119 | # Generates a link to a message |
||
| 120 | def link_to_message(message, options={}, html_options = nil)
|
||
| 121 | link_to( |
||
| 122 | h(truncate(message.subject, :length => 60)), |
||
| 123 | { :controller => 'messages', :action => 'show',
|
||
| 124 | :board_id => message.board_id, |
||
| 125 | :id => (message.parent_id || message.id), |
||
| 126 | :r => (message.parent_id && message.id), |
||
| 127 | :anchor => (message.parent_id ? "message-#{message.id}" : nil)
|
||
| 128 | }.merge(options), |
||
| 129 | html_options |
||
| 130 | ) |
||
| 131 | end |
||
| 132 | |||
| 133 | # Generates a link to a project if active |
||
| 134 | # Examples: |
||
| 135 | # |
||
| 136 | # link_to_project(project) # => link to the specified project overview |
||
| 137 | # link_to_project(project, :action=>'settings') # => link to project settings |
||
| 138 | # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
|
||
| 139 | # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
|
||
| 140 | # |
||
| 141 | def link_to_project(project, options={}, html_options = nil)
|
||
| 142 | if project.archived? |
||
| 143 | h(project) |
||
| 144 | else |
||
| 145 | url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
|
||
| 146 | link_to(h(project), url, html_options) |
||
| 147 | end |
||
| 148 | end |
||
| 149 | |||
| 150 | def wiki_page_path(page, options={})
|
||
| 151 | url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
|
||
| 152 | end |
||
| 153 | |||
| 154 | def thumbnail_tag(attachment) |
||
| 155 | link_to image_tag(url_for(:controller => 'attachments', :action => 'thumbnail', :id => attachment)), |
||
| 156 | {:controller => 'attachments', :action => 'show', :id => attachment, :filename => attachment.filename},
|
||
| 157 | :title => attachment.filename |
||
| 158 | end |
||
| 159 | |||
| 160 | def toggle_link(name, id, options={})
|
||
| 161 | onclick = "$('##{id}').toggle(); "
|
||
| 162 | onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
|
||
| 163 | onclick << "return false;" |
||
| 164 | link_to(name, "#", :onclick => onclick) |
||
| 165 | end |
||
| 166 | |||
| 167 | def image_to_function(name, function, html_options = {})
|
||
| 168 | html_options.symbolize_keys! |
||
| 169 | tag(:input, html_options.merge({
|
||
| 170 | :type => "image", :src => image_path(name), |
||
| 171 | :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
|
||
| 172 | })) |
||
| 173 | end |
||
| 174 | |||
| 175 | def format_activity_title(text) |
||
| 176 | h(truncate_single_line(text, :length => 100)) |
||
| 177 | end |
||
| 178 | |||
| 179 | def format_activity_day(date) |
||
| 180 | date == User.current.today ? l(:label_today).titleize : format_date(date) |
||
| 181 | end |
||
| 182 | |||
| 183 | def format_activity_description(text) |
||
| 184 | h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
|
||
| 185 | ).gsub(/[\r\n]+/, "<br />").html_safe |
||
| 186 | end |
||
| 187 | |||
| 188 | def format_version_name(version) |
||
| 189 | if version.project == @project |
||
| 190 | h(version) |
||
| 191 | else |
||
| 192 | h("#{version.project} - #{version}")
|
||
| 193 | end |
||
| 194 | end |
||
| 195 | |||
| 196 | def due_date_distance_in_words(date) |
||
| 197 | if date |
||
| 198 | l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date)) |
||
| 199 | end |
||
| 200 | end |
||
| 201 | |||
| 202 | # Renders a tree of projects as a nested set of unordered lists |
||
| 203 | # The given collection may be a subset of the whole project tree |
||
| 204 | # (eg. some intermediate nodes are private and can not be seen) |
||
| 205 | def render_project_nested_lists(projects) |
||
| 206 | s = '' |
||
| 207 | if projects.any? |
||
| 208 | ancestors = [] |
||
| 209 | original_project = @project |
||
| 210 | projects.sort_by(&:lft).each do |project| |
||
| 211 | # set the project environment to please macros. |
||
| 212 | @project = project |
||
| 213 | if (ancestors.empty? || project.is_descendant_of?(ancestors.last)) |
||
| 214 | s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
|
||
| 215 | else |
||
| 216 | ancestors.pop |
||
| 217 | s << "</li>" |
||
| 218 | while (ancestors.any? && !project.is_descendant_of?(ancestors.last)) |
||
| 219 | ancestors.pop |
||
| 220 | s << "</ul></li>\n" |
||
| 221 | end |
||
| 222 | end |
||
| 223 | classes = (ancestors.empty? ? 'root' : 'child') |
||
| 224 | s << "<li class='#{classes}'><div class='#{classes}'>"
|
||
| 225 | s << h(block_given? ? yield(project) : project.name) |
||
| 226 | s << "</div>\n" |
||
| 227 | ancestors << project |
||
| 228 | end |
||
| 229 | s << ("</li></ul>\n" * ancestors.size)
|
||
| 230 | @project = original_project |
||
| 231 | end |
||
| 232 | s.html_safe |
||
| 233 | end |
||
| 234 | |||
| 235 | def render_page_hierarchy(pages, node=nil, options={})
|
||
| 236 | content = '' |
||
| 237 | if pages[node] |
||
| 238 | content << "<ul class=\"pages-hierarchy\">\n" |
||
| 239 | pages[node].each do |page| |
||
| 240 | content << "<li>" |
||
| 241 | content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
|
||
| 242 | :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil)) |
||
| 243 | content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id] |
||
| 244 | content << "</li>\n" |
||
| 245 | end |
||
| 246 | content << "</ul>\n" |
||
| 247 | end |
||
| 248 | content.html_safe |
||
| 249 | end |
||
| 250 | |||
| 251 | # Renders flash messages |
||
| 252 | def render_flash_messages |
||
| 253 | s = '' |
||
| 254 | flash.each do |k,v| |
||
| 255 | s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
|
||
| 256 | end |
||
| 257 | s.html_safe |
||
| 258 | end |
||
| 259 | |||
| 260 | # Renders tabs and their content |
||
| 261 | def render_tabs(tabs) |
||
| 262 | if tabs.any? |
||
| 263 | render :partial => 'common/tabs', :locals => {:tabs => tabs}
|
||
| 264 | else |
||
| 265 | content_tag 'p', l(:label_no_data), :class => "nodata" |
||
| 266 | end |
||
| 267 | end |
||
| 268 | |||
| 269 | # Renders the project quick-jump box |
||
| 270 | def render_project_jump_box |
||
| 271 | return unless User.current.logged? |
||
| 272 | projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq |
||
| 273 | if projects.any? |
||
| 274 | options = |
||
| 275 | ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
|
||
| 276 | '<option value="" disabled="disabled">---</option>').html_safe |
||
| 277 | |||
| 278 | options << project_tree_options_for_select(projects, :selected => @project) do |p| |
||
| 279 | { :value => project_path(:id => p, :jump => current_menu_item) }
|
||
| 280 | end |
||
| 281 | |||
| 282 | select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
|
||
| 283 | end |
||
| 284 | end |
||
| 285 | |||
| 286 | def project_tree_options_for_select(projects, options = {})
|
||
| 287 | s = '' |
||
| 288 | project_tree(projects) do |project, level| |
||
| 289 | name_prefix = (level > 0 ? ' ' * 2 * level + '» ' : '').html_safe |
||
| 290 | tag_options = {:value => project.id}
|
||
| 291 | if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project)) |
||
| 292 | tag_options[:selected] = 'selected' |
||
| 293 | else |
||
| 294 | tag_options[:selected] = nil |
||
| 295 | end |
||
| 296 | tag_options.merge!(yield(project)) if block_given? |
||
| 297 | s << content_tag('option', name_prefix + h(project), tag_options)
|
||
| 298 | end |
||
| 299 | s.html_safe |
||
| 300 | end |
||
| 301 | |||
| 302 | # Yields the given block for each project with its level in the tree |
||
| 303 | # |
||
| 304 | # Wrapper for Project#project_tree |
||
| 305 | def project_tree(projects, &block) |
||
| 306 | Project.project_tree(projects, &block) |
||
| 307 | end |
||
| 308 | |||
| 309 | def principals_check_box_tags(name, principals) |
||
| 310 | s = '' |
||
| 311 | principals.sort.each do |principal| |
||
| 312 | s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
|
||
| 313 | end |
||
| 314 | s.html_safe |
||
| 315 | end |
||
| 316 | |||
| 317 | # Returns a string for users/groups option tags |
||
| 318 | def principals_options_for_select(collection, selected=nil) |
||
| 319 | s = '' |
||
| 320 | if collection.include?(User.current) |
||
| 321 | s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
|
||
| 322 | end |
||
| 323 | groups = '' |
||
| 324 | collection.sort.each do |element| |
||
| 325 | selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) |
||
| 326 | (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
|
||
| 327 | end |
||
| 328 | unless groups.empty? |
||
| 329 | s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
|
||
| 330 | end |
||
| 331 | s.html_safe |
||
| 332 | end |
||
| 333 | |||
| 334 | # Options for the new membership projects combo-box |
||
| 335 | def options_for_membership_project_select(principal, projects) |
||
| 336 | options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
|
||
| 337 | options << project_tree_options_for_select(projects) do |p| |
||
| 338 | {:disabled => principal.projects.include?(p)}
|
||
| 339 | end |
||
| 340 | options |
||
| 341 | end |
||
| 342 | |||
| 343 | # Truncates and returns the string as a single line |
||
| 344 | def truncate_single_line(string, *args) |
||
| 345 | truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
|
||
| 346 | end |
||
| 347 | |||
| 348 | # Truncates at line break after 250 characters or options[:length] |
||
| 349 | def truncate_lines(string, options={})
|
||
| 350 | length = options[:length] || 250 |
||
| 351 | if string.to_s =~ /\A(.{#{length}}.*?)$/m
|
||
| 352 | "#{$1}..."
|
||
| 353 | else |
||
| 354 | string |
||
| 355 | end |
||
| 356 | end |
||
| 357 | |||
| 358 | def anchor(text) |
||
| 359 | text.to_s.gsub(' ', '_')
|
||
| 360 | end |
||
| 361 | |||
| 362 | def html_hours(text) |
||
| 363 | text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
|
||
| 364 | end |
||
| 365 | |||
| 366 | def authoring(created, author, options={})
|
||
| 367 | l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe |
||
| 368 | end |
||
| 369 | |||
| 370 | def time_tag(time) |
||
| 371 | text = distance_of_time_in_words(Time.now, time) |
||
| 372 | if @project |
||
| 373 | link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
|
||
| 374 | else |
||
| 375 | content_tag('acronym', text, :title => format_time(time))
|
||
| 376 | end |
||
| 377 | end |
||
| 378 | |||
| 379 | def syntax_highlight_lines(name, content) |
||
| 380 | lines = [] |
||
| 381 | syntax_highlight(name, content).each_line { |line| lines << line }
|
||
| 382 | lines |
||
| 383 | end |
||
| 384 | |||
| 385 | def syntax_highlight(name, content) |
||
| 386 | Redmine::SyntaxHighlighting.highlight_by_filename(content, name) |
||
| 387 | end |
||
| 388 | |||
| 389 | def to_path_param(path) |
||
| 390 | str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
|
||
| 391 | str.blank? ? nil : str |
||
| 392 | end |
||
| 393 | |||
| 394 | def pagination_links_full(paginator, count=nil, options={})
|
||
| 395 | page_param = options.delete(:page_param) || :page |
||
| 396 | per_page_links = options.delete(:per_page_links) |
||
| 397 | url_param = params.dup |
||
| 398 | |||
| 399 | html = '' |
||
| 400 | if paginator.current.previous |
||
| 401 | # \xc2\xab(utf-8) = « |
||
| 402 | html << link_to_content_update( |
||
| 403 | "\xc2\xab " + l(:label_previous), |
||
| 404 | url_param.merge(page_param => paginator.current.previous)) + ' ' |
||
| 405 | end |
||
| 406 | |||
| 407 | html << (pagination_links_each(paginator, options) do |n| |
||
| 408 | link_to_content_update(n.to_s, url_param.merge(page_param => n)) |
||
| 409 | end || '') |
||
| 410 | |||
| 411 | if paginator.current.next |
||
| 412 | # \xc2\xbb(utf-8) = » |
||
| 413 | html << ' ' + link_to_content_update( |
||
| 414 | (l(:label_next) + " \xc2\xbb"), |
||
| 415 | url_param.merge(page_param => paginator.current.next)) |
||
| 416 | end |
||
| 417 | |||
| 418 | unless count.nil? |
||
| 419 | html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
|
||
| 420 | if per_page_links != false && links = per_page_links(paginator.items_per_page, count) |
||
| 421 | html << " | #{links}"
|
||
| 422 | end |
||
| 423 | end |
||
| 424 | |||
| 425 | html.html_safe |
||
| 426 | end |
||
| 427 | |||
| 428 | def per_page_links(selected=nil, item_count=nil) |
||
| 429 | values = Setting.per_page_options_array |
||
| 430 | if item_count && values.any? |
||
| 431 | if item_count > values.first |
||
| 432 | max = values.detect {|value| value >= item_count} || item_count
|
||
| 433 | else |
||
| 434 | max = item_count |
||
| 435 | end |
||
| 436 | values = values.select {|value| value <= max || value == selected}
|
||
| 437 | end |
||
| 438 | if values.empty? || (values.size == 1 && values.first == selected) |
||
| 439 | return nil |
||
| 440 | end |
||
| 441 | links = values.collect do |n| |
||
| 442 | n == selected ? n : link_to_content_update(n, params.merge(:per_page => n)) |
||
| 443 | end |
||
| 444 | l(:label_display_per_page, links.join(', '))
|
||
| 445 | end |
||
| 446 | |||
| 447 | def reorder_links(name, url, method = :post) |
||
| 448 | link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
|
||
| 449 | url.merge({"#{name}[move_to]" => 'highest'}),
|
||
| 450 | :method => method, :title => l(:label_sort_highest)) + |
||
| 451 | link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
|
||
| 452 | url.merge({"#{name}[move_to]" => 'higher'}),
|
||
| 453 | :method => method, :title => l(:label_sort_higher)) + |
||
| 454 | link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
|
||
| 455 | url.merge({"#{name}[move_to]" => 'lower'}),
|
||
| 456 | :method => method, :title => l(:label_sort_lower)) + |
||
| 457 | link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
|
||
| 458 | url.merge({"#{name}[move_to]" => 'lowest'}),
|
||
| 459 | :method => method, :title => l(:label_sort_lowest)) |
||
| 460 | end |
||
| 461 | |||
| 462 | def breadcrumb(*args) |
||
| 463 | elements = args.flatten |
||
| 464 | elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
|
||
| 465 | end |
||
| 466 | |||
| 467 | def other_formats_links(&block) |
||
| 468 | concat('<p class="other-formats">'.html_safe + l(:label_export_to))
|
||
| 469 | yield Redmine::Views::OtherFormatsBuilder.new(self) |
||
| 470 | concat('</p>'.html_safe)
|
||
| 471 | end |
||
| 472 | |||
| 473 | def page_header_title |
||
| 474 | if @project.nil? || @project.new_record? |
||
| 475 | h(Setting.app_title) |
||
| 476 | else |
||
| 477 | b = [] |
||
| 478 | ancestors = (@project.root? ? [] : @project.ancestors.visible.all) |
||
| 479 | if ancestors.any? |
||
| 480 | root = ancestors.shift |
||
| 481 | b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
|
||
| 482 | if ancestors.size > 2 |
||
| 483 | b << "\xe2\x80\xa6" |
||
| 484 | ancestors = ancestors[-2, 2] |
||
| 485 | end |
||
| 486 | b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
|
||
| 487 | end |
||
| 488 | b << h(@project) |
||
| 489 | b.join(" \xc2\xbb ").html_safe
|
||
| 490 | end |
||
| 491 | end |
||
| 492 | |||
| 493 | def html_title(*args) |
||
| 494 | if args.empty? |
||
| 495 | title = @html_title || [] |
||
| 496 | title << @project.name if @project |
||
| 497 | title << Setting.app_title unless Setting.app_title == title.last |
||
| 498 | title.select {|t| !t.blank? }.join(' - ')
|
||
| 499 | else |
||
| 500 | @html_title ||= [] |
||
| 501 | @html_title += args |
||
| 502 | end |
||
| 503 | end |
||
| 504 | |||
| 505 | # Returns the theme, controller name, and action as css classes for the |
||
| 506 | # HTML body. |
||
| 507 | def body_css_classes |
||
| 508 | css = [] |
||
| 509 | if theme = Redmine::Themes.theme(Setting.ui_theme) |
||
| 510 | css << 'theme-' + theme.name |
||
| 511 | end |
||
| 512 | |||
| 513 | css << 'controller-' + controller_name |
||
| 514 | css << 'action-' + action_name |
||
| 515 | css.join(' ')
|
||
| 516 | end |
||
| 517 | |||
| 518 | def accesskey(s) |
||
| 519 | Redmine::AccessKeys.key_for s |
||
| 520 | end |
||
| 521 | |||
| 522 | # Formats text according to system settings. |
||
| 523 | # 2 ways to call this method: |
||
| 524 | # * with a String: textilizable(text, options) |
||
| 525 | # * with an object and one of its attribute: textilizable(issue, :description, options) |
||
| 526 | def textilizable(*args) |
||
| 527 | options = args.last.is_a?(Hash) ? args.pop : {}
|
||
| 528 | case args.size |
||
| 529 | when 1 |
||
| 530 | obj = options[:object] |
||
| 531 | text = args.shift |
||
| 532 | when 2 |
||
| 533 | obj = args.shift |
||
| 534 | attr = args.shift |
||
| 535 | text = obj.send(attr).to_s |
||
| 536 | else |
||
| 537 | raise ArgumentError, 'invalid arguments to textilizable' |
||
| 538 | end |
||
| 539 | return '' if text.blank? |
||
| 540 | project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil) |
||
| 541 | only_path = options.delete(:only_path) == false ? false : true |
||
| 542 | |||
| 543 | text = text.dup |
||
| 544 | macros = catch_macros(text) |
||
| 545 | text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) |
||
| 546 | |||
| 547 | @parsed_headings = [] |
||
| 548 | @heading_anchors = {}
|
||
| 549 | @current_section = 0 if options[:edit_section_links] |
||
| 550 | |||
| 551 | parse_sections(text, project, obj, attr, only_path, options) |
||
| 552 | text = parse_non_pre_blocks(text, obj, macros) do |text| |
||
| 553 | [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name| |
||
| 554 | send method_name, text, project, obj, attr, only_path, options |
||
| 555 | end |
||
| 556 | end |
||
| 557 | parse_headings(text, project, obj, attr, only_path, options) |
||
| 558 | |||
| 559 | if @parsed_headings.any? |
||
| 560 | replace_toc(text, @parsed_headings) |
||
| 561 | end |
||
| 562 | |||
| 563 | text.html_safe |
||
| 564 | end |
||
| 565 | |||
| 566 | def parse_non_pre_blocks(text, obj, macros) |
||
| 567 | s = StringScanner.new(text) |
||
| 568 | tags = [] |
||
| 569 | parsed = '' |
||
| 570 | while !s.eos? |
||
| 571 | s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im) |
||
| 572 | text, full_tag, closing, tag = s[1], s[2], s[3], s[4] |
||
| 573 | if tags.empty? |
||
| 574 | yield text |
||
| 575 | inject_macros(text, obj, macros) if macros.any? |
||
| 576 | else |
||
| 577 | inject_macros(text, obj, macros, false) if macros.any? |
||
| 578 | end |
||
| 579 | parsed << text |
||
| 580 | if tag |
||
| 581 | if closing |
||
| 582 | if tags.last == tag.downcase |
||
| 583 | tags.pop |
||
| 584 | end |
||
| 585 | else |
||
| 586 | tags << tag.downcase |
||
| 587 | end |
||
| 588 | parsed << full_tag |
||
| 589 | end |
||
| 590 | end |
||
| 591 | # Close any non closing tags |
||
| 592 | while tag = tags.pop |
||
| 593 | parsed << "</#{tag}>"
|
||
| 594 | end |
||
| 595 | parsed |
||
| 596 | end |
||
| 597 | |||
| 598 | def parse_inline_attachments(text, project, obj, attr, only_path, options) |
||
| 599 | # when using an image link, try to use an attachment, if possible |
||
| 600 | if options[:attachments] || (obj && obj.respond_to?(:attachments)) |
||
| 601 | attachments = options[:attachments] || obj.attachments |
||
| 602 | text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m| |
||
| 603 | filename, ext, alt, alttext = $1.downcase, $2, $3, $4 |
||
| 604 | # search for the picture in attachments |
||
| 605 | if found = Attachment.latest_attach(attachments, filename) |
||
| 606 | image_url = url_for :only_path => only_path, :controller => 'attachments', |
||
| 607 | :action => 'download', :id => found |
||
| 608 | desc = found.description.to_s.gsub('"', '')
|
||
| 609 | if !desc.blank? && alttext.blank? |
||
| 610 | alt = " title=\"#{desc}\" alt=\"#{desc}\""
|
||
| 611 | end |
||
| 612 | "src=\"#{image_url}\"#{alt}"
|
||
| 613 | else |
||
| 614 | m |
||
| 615 | end |
||
| 616 | end |
||
| 617 | end |
||
| 618 | end |
||
| 619 | |||
| 620 | # Wiki links |
||
| 621 | # |
||
| 622 | # Examples: |
||
| 623 | # [[mypage]] |
||
| 624 | # [[mypage|mytext]] |
||
| 625 | # wiki links can refer other project wikis, using project name or identifier: |
||
| 626 | # [[project:]] -> wiki starting page |
||
| 627 | # [[project:|mytext]] |
||
| 628 | # [[project:mypage]] |
||
| 629 | # [[project:mypage|mytext]] |
||
| 630 | def parse_wiki_links(text, project, obj, attr, only_path, options) |
||
| 631 | text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m| |
||
| 632 | link_project = project |
||
| 633 | esc, all, page, title = $1, $2, $3, $5 |
||
| 634 | if esc.nil? |
||
| 635 | if page =~ /^([^\:]+)\:(.*)$/ |
||
| 636 | link_project = Project.find_by_identifier($1) || Project.find_by_name($1) |
||
| 637 | page = $2 |
||
| 638 | title ||= $1 if page.blank? |
||
| 639 | end |
||
| 640 | |||
| 641 | if link_project && link_project.wiki |
||
| 642 | # extract anchor |
||
| 643 | anchor = nil |
||
| 644 | if page =~ /^(.+?)\#(.+)$/ |
||
| 645 | page, anchor = $1, $2 |
||
| 646 | end |
||
| 647 | anchor = sanitize_anchor_name(anchor) if anchor.present? |
||
| 648 | # check if page exists |
||
| 649 | wiki_page = link_project.wiki.find_page(page) |
||
| 650 | url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page |
||
| 651 | "##{anchor}"
|
||
| 652 | else |
||
| 653 | case options[:wiki_links] |
||
| 654 | when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
|
||
| 655 | when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
|
||
| 656 | else |
||
| 657 | wiki_page_id = page.present? ? Wiki.titleize(page) : nil |
||
| 658 | parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil |
||
| 659 | url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, |
||
| 660 | :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent) |
||
| 661 | end |
||
| 662 | end |
||
| 663 | link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
|
||
| 664 | else |
||
| 665 | # project or wiki doesn't exist |
||
| 666 | all |
||
| 667 | end |
||
| 668 | else |
||
| 669 | all |
||
| 670 | end |
||
| 671 | end |
||
| 672 | end |
||
| 673 | |||
| 674 | # Redmine links |
||
| 675 | # |
||
| 676 | # Examples: |
||
| 677 | # Issues: |
||
| 678 | # #52 -> Link to issue #52 |
||
| 679 | # Changesets: |
||
| 680 | # r52 -> Link to revision 52 |
||
| 681 | # commit:a85130f -> Link to scmid starting with a85130f |
||
| 682 | # Documents: |
||
| 683 | # document#17 -> Link to document with id 17 |
||
| 684 | # document:Greetings -> Link to the document with title "Greetings" |
||
| 685 | # document:"Some document" -> Link to the document with title "Some document" |
||
| 686 | # Versions: |
||
| 687 | # version#3 -> Link to version with id 3 |
||
| 688 | # version:1.0.0 -> Link to version named "1.0.0" |
||
| 689 | # version:"1.0 beta 2" -> Link to version named "1.0 beta 2" |
||
| 690 | # Attachments: |
||
| 691 | # attachment:file.zip -> Link to the attachment of the current object named file.zip |
||
| 692 | # Source files: |
||
| 693 | # source:some/file -> Link to the file located at /some/file in the project's repository |
||
| 694 | # source:some/file@52 -> Link to the file's revision 52 |
||
| 695 | # source:some/file#L120 -> Link to line 120 of the file |
||
| 696 | # source:some/file@52#L120 -> Link to line 120 of the file's revision 52 |
||
| 697 | # export:some/file -> Force the download of the file |
||
| 698 | # Forum messages: |
||
| 699 | # message#1218 -> Link to message with id 1218 |
||
| 700 | # |
||
| 701 | # Links can refer other objects from other projects, using project identifier: |
||
| 702 | # identifier:r52 |
||
| 703 | # identifier:document:"Some document" |
||
| 704 | # identifier:version:1.0.0 |
||
| 705 | # identifier:source:some/file |
||
| 706 | def parse_redmine_links(text, project, obj, attr, only_path, options) |
||
| 707 | text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
|
||
| 708 | leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $2, $3, $4, $5, $10, $11, $8 || $12 || $18, $14 || $19, $15, $17 |
||
| 709 | link = nil |
||
| 710 | if project_identifier |
||
| 711 | project = Project.visible.find_by_identifier(project_identifier) |
||
| 712 | end |
||
| 713 | if esc.nil? |
||
| 714 | if prefix.nil? && sep == 'r' |
||
| 715 | if project |
||
| 716 | repository = nil |
||
| 717 | if repo_identifier |
||
| 718 | repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
|
||
| 719 | else |
||
| 720 | repository = project.repository |
||
| 721 | end |
||
| 722 | # project.changesets.visible raises an SQL error because of a double join on repositories |
||
| 723 | if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier)) |
||
| 724 | link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.revision},
|
||
| 725 | :class => 'changeset', |
||
| 726 | :title => truncate_single_line(changeset.comments, :length => 100)) |
||
| 727 | end |
||
| 728 | end |
||
| 729 | elsif sep == '#' |
||
| 730 | oid = identifier.to_i |
||
| 731 | case prefix |
||
| 732 | when nil |
||
| 733 | if oid.to_s == identifier && issue = Issue.visible.find_by_id(oid, :include => :status) |
||
| 734 | anchor = comment_id ? "note-#{comment_id}" : nil
|
||
| 735 | link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor},
|
||
| 736 | :class => issue.css_classes, |
||
| 737 | :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
|
||
| 738 | end |
||
| 739 | when 'document' |
||
| 740 | if document = Document.visible.find_by_id(oid) |
||
| 741 | link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
|
||
| 742 | :class => 'document' |
||
| 743 | end |
||
| 744 | when 'version' |
||
| 745 | if version = Version.visible.find_by_id(oid) |
||
| 746 | link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
|
||
| 747 | :class => 'version' |
||
| 748 | end |
||
| 749 | when 'message' |
||
| 750 | if message = Message.visible.find_by_id(oid, :include => :parent) |
||
| 751 | link = link_to_message(message, {:only_path => only_path}, :class => 'message')
|
||
| 752 | end |
||
| 753 | when 'forum' |
||
| 754 | if board = Board.visible.find_by_id(oid) |
||
| 755 | link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
|
||
| 756 | :class => 'board' |
||
| 757 | end |
||
| 758 | when 'news' |
||
| 759 | if news = News.visible.find_by_id(oid) |
||
| 760 | link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
|
||
| 761 | :class => 'news' |
||
| 762 | end |
||
| 763 | when 'project' |
||
| 764 | if p = Project.visible.find_by_id(oid) |
||
| 765 | link = link_to_project(p, {:only_path => only_path}, :class => 'project')
|
||
| 766 | end |
||
| 767 | end |
||
| 768 | elsif sep == ':' |
||
| 769 | # removes the double quotes if any |
||
| 770 | name = identifier.gsub(%r{^"(.*)"$}, "\\1")
|
||
| 771 | case prefix |
||
| 772 | when 'document' |
||
| 773 | if project && document = project.documents.visible.find_by_title(name) |
||
| 774 | link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
|
||
| 775 | :class => 'document' |
||
| 776 | end |
||
| 777 | when 'version' |
||
| 778 | if project && version = project.versions.visible.find_by_name(name) |
||
| 779 | link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
|
||
| 780 | :class => 'version' |
||
| 781 | end |
||
| 782 | when 'forum' |
||
| 783 | if project && board = project.boards.visible.find_by_name(name) |
||
| 784 | link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
|
||
| 785 | :class => 'board' |
||
| 786 | end |
||
| 787 | when 'news' |
||
| 788 | if project && news = project.news.visible.find_by_title(name) |
||
| 789 | link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
|
||
| 790 | :class => 'news' |
||
| 791 | end |
||
| 792 | when 'commit', 'source', 'export' |
||
| 793 | if project |
||
| 794 | repository = nil |
||
| 795 | if name =~ %r{^(([a-z0-9\-]+)\|)(.+)$}
|
||
| 796 | repo_prefix, repo_identifier, name = $1, $2, $3 |
||
| 797 | repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
|
||
| 798 | else |
||
| 799 | repository = project.repository |
||
| 800 | end |
||
| 801 | if prefix == 'commit' |
||
| 802 | if repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%"]))
|
||
| 803 | link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
|
||
| 804 | :class => 'changeset', |
||
| 805 | :title => truncate_single_line(h(changeset.comments), :length => 100) |
||
| 806 | end |
||
| 807 | else |
||
| 808 | if repository && User.current.allowed_to?(:browse_repository, project) |
||
| 809 | name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
|
||
| 810 | path, rev, anchor = $1, $3, $5 |
||
| 811 | link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
|
||
| 812 | :path => to_path_param(path), |
||
| 813 | :rev => rev, |
||
| 814 | :anchor => anchor}, |
||
| 815 | :class => (prefix == 'export' ? 'source download' : 'source') |
||
| 816 | end |
||
| 817 | end |
||
| 818 | repo_prefix = nil |
||
| 819 | end |
||
| 820 | when 'attachment' |
||
| 821 | attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil) |
||
| 822 | if attachments && attachment = attachments.detect {|a| a.filename == name }
|
||
| 823 | link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
|
||
| 824 | :class => 'attachment' |
||
| 825 | end |
||
| 826 | when 'project' |
||
| 827 | if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
|
||
| 828 | link = link_to_project(p, {:only_path => only_path}, :class => 'project')
|
||
| 829 | end |
||
| 830 | end |
||
| 831 | end |
||
| 832 | end |
||
| 833 | (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
|
||
| 834 | end |
||
| 835 | end |
||
| 836 | |||
| 837 | HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE) |
||
| 838 | |||
| 839 | def parse_sections(text, project, obj, attr, only_path, options) |
||
| 840 | return unless options[:edit_section_links] |
||
| 841 | text.gsub!(HEADING_RE) do |
||
| 842 | heading = $1 |
||
| 843 | @current_section += 1 |
||
| 844 | if @current_section > 1 |
||
| 845 | content_tag('div',
|
||
| 846 | link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
|
||
| 847 | :class => 'contextual', |
||
| 848 | :title => l(:button_edit_section)) + heading.html_safe |
||
| 849 | else |
||
| 850 | heading |
||
| 851 | end |
||
| 852 | end |
||
| 853 | end |
||
| 854 | |||
| 855 | # Headings and TOC |
||
| 856 | # Adds ids and links to headings unless options[:headings] is set to false |
||
| 857 | def parse_headings(text, project, obj, attr, only_path, options) |
||
| 858 | return if options[:headings] == false |
||
| 859 | |||
| 860 | text.gsub!(HEADING_RE) do |
||
| 861 | level, attrs, content = $2.to_i, $3, $4 |
||
| 862 | item = strip_tags(content).strip |
||
| 863 | anchor = sanitize_anchor_name(item) |
||
| 864 | # used for single-file wiki export |
||
| 865 | anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
|
||
| 866 | @heading_anchors[anchor] ||= 0 |
||
| 867 | idx = (@heading_anchors[anchor] += 1) |
||
| 868 | if idx > 1 |
||
| 869 | anchor = "#{anchor}-#{idx}"
|
||
| 870 | end |
||
| 871 | @parsed_headings << [level, anchor, item] |
||
| 872 | "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">¶</a></h#{level}>"
|
||
| 873 | end |
||
| 874 | end |
||
| 875 | |||
| 876 | MACROS_RE = /( |
||
| 877 | (!)? # escaping |
||
| 878 | ( |
||
| 879 | \{\{ # opening tag
|
||
| 880 | ([\w]+) # macro name |
||
| 881 | (\(([^\n\r]*?)\))? # optional arguments |
||
| 882 | ([\n\r].*?[\n\r])? # optional block of text |
||
| 883 | \}\} # closing tag |
||
| 884 | ) |
||
| 885 | )/mx unless const_defined?(:MACROS_RE) |
||
| 886 | |||
| 887 | MACRO_SUB_RE = /( |
||
| 888 | \{\{
|
||
| 889 | macro\((\d+)\) |
||
| 890 | \}\} |
||
| 891 | )/x unless const_defined?(:MACRO_SUB_RE) |
||
| 892 | |||
| 893 | # Extracts macros from text |
||
| 894 | def catch_macros(text) |
||
| 895 | macros = {}
|
||
| 896 | text.gsub!(MACROS_RE) do |
||
| 897 | all, macro = $1, $4.downcase |
||
| 898 | if macro_exists?(macro) || all =~ MACRO_SUB_RE |
||
| 899 | index = macros.size |
||
| 900 | macros[index] = all |
||
| 901 | "{{macro(#{index})}}"
|
||
| 902 | else |
||
| 903 | all |
||
| 904 | end |
||
| 905 | end |
||
| 906 | macros |
||
| 907 | end |
||
| 908 | |||
| 909 | # Executes and replaces macros in text |
||
| 910 | def inject_macros(text, obj, macros, execute=true) |
||
| 911 | text.gsub!(MACRO_SUB_RE) do |
||
| 912 | all, index = $1, $2.to_i |
||
| 913 | orig = macros.delete(index) |
||
| 914 | if execute && orig && orig =~ MACROS_RE |
||
| 915 | esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip) |
||
| 916 | if esc.nil? |
||
| 917 | h(exec_macro(macro, obj, args, block) || all) |
||
| 918 | else |
||
| 919 | h(all) |
||
| 920 | end |
||
| 921 | elsif orig |
||
| 922 | h(orig) |
||
| 923 | else |
||
| 924 | h(all) |
||
| 925 | end |
||
| 926 | end |
||
| 927 | end |
||
| 928 | |||
| 929 | TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
|
||
| 930 | |||
| 931 | # Renders the TOC with given headings |
||
| 932 | def replace_toc(text, headings) |
||
| 933 | text.gsub!(TOC_RE) do |
||
| 934 | # Keep only the 4 first levels |
||
| 935 | headings = headings.select{|level, anchor, item| level <= 4}
|
||
| 936 | if headings.empty? |
||
| 937 | '' |
||
| 938 | else |
||
| 939 | div_class = 'toc' |
||
| 940 | div_class << ' right' if $1 == '>' |
||
| 941 | div_class << ' left' if $1 == '<' |
||
| 942 | out = "<ul class=\"#{div_class}\"><li>"
|
||
| 943 | root = headings.map(&:first).min |
||
| 944 | current = root |
||
| 945 | started = false |
||
| 946 | headings.each do |level, anchor, item| |
||
| 947 | if level > current |
||
| 948 | out << '<ul><li>' * (level - current) |
||
| 949 | elsif level < current |
||
| 950 | out << "</li></ul>\n" * (current - level) + "</li><li>" |
||
| 951 | elsif started |
||
| 952 | out << '</li><li>' |
||
| 953 | end |
||
| 954 | out << "<a href=\"##{anchor}\">#{item}</a>"
|
||
| 955 | current = level |
||
| 956 | started = true |
||
| 957 | end |
||
| 958 | out << '</li></ul>' * (current - root) |
||
| 959 | out << '</li></ul>' |
||
| 960 | end |
||
| 961 | end |
||
| 962 | end |
||
| 963 | |||
| 964 | # Same as Rails' simple_format helper without using paragraphs |
||
| 965 | def simple_format_without_paragraph(text) |
||
| 966 | text.to_s. |
||
| 967 | gsub(/\r\n?/, "\n"). # \r\n and \r -> \n |
||
| 968 | gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br |
||
| 969 | gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br |
||
| 970 | html_safe |
||
| 971 | end |
||
| 972 | |||
| 973 | def lang_options_for_select(blank=true) |
||
| 974 | (blank ? [["(auto)", ""]] : []) + languages_options |
||
| 975 | end |
||
| 976 | |||
| 977 | def label_tag_for(name, option_tags = nil, options = {})
|
||
| 978 | label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
|
||
| 979 | content_tag("label", label_text)
|
||
| 980 | end |
||
| 981 | |||
| 982 | def labelled_form_for(*args, &proc) |
||
| 983 | args << {} unless args.last.is_a?(Hash)
|
||
| 984 | options = args.last |
||
| 985 | if args.first.is_a?(Symbol) |
||
| 986 | options.merge!(:as => args.shift) |
||
| 987 | end |
||
| 988 | options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
|
||
| 989 | form_for(*args, &proc) |
||
| 990 | end |
||
| 991 | |||
| 992 | def labelled_fields_for(*args, &proc) |
||
| 993 | args << {} unless args.last.is_a?(Hash)
|
||
| 994 | options = args.last |
||
| 995 | options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
|
||
| 996 | fields_for(*args, &proc) |
||
| 997 | end |
||
| 998 | |||
| 999 | def labelled_remote_form_for(*args, &proc) |
||
| 1000 | ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_remote_form_for is deprecated and will be removed in Redmine 2.2." |
||
| 1001 | args << {} unless args.last.is_a?(Hash)
|
||
| 1002 | options = args.last |
||
| 1003 | options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
|
||
| 1004 | form_for(*args, &proc) |
||
| 1005 | end |
||
| 1006 | |||
| 1007 | def error_messages_for(*objects) |
||
| 1008 | html = "" |
||
| 1009 | objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
|
||
| 1010 | errors = objects.map {|o| o.errors.full_messages}.flatten
|
||
| 1011 | if errors.any? |
||
| 1012 | html << "<div id='errorExplanation'><ul>\n" |
||
| 1013 | errors.each do |error| |
||
| 1014 | html << "<li>#{h error}</li>\n"
|
||
| 1015 | end |
||
| 1016 | html << "</ul></div>\n" |
||
| 1017 | end |
||
| 1018 | html.html_safe |
||
| 1019 | end |
||
| 1020 | |||
| 1021 | def delete_link(url, options={})
|
||
| 1022 | options = {
|
||
| 1023 | :method => :delete, |
||
| 1024 | :data => {:confirm => l(:text_are_you_sure)},
|
||
| 1025 | :class => 'icon icon-del' |
||
| 1026 | }.merge(options) |
||
| 1027 | |||
| 1028 | link_to l(:button_delete), url, options |
||
| 1029 | end |
||
| 1030 | |||
| 1031 | def preview_link(url, form, target='preview', options={})
|
||
| 1032 | content_tag 'a', l(:label_preview), {
|
||
| 1033 | :href => "#", |
||
| 1034 | :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
|
||
| 1035 | :accesskey => accesskey(:preview) |
||
| 1036 | }.merge(options) |
||
| 1037 | end |
||
| 1038 | |||
| 1039 | def link_to_function(name, function, html_options={})
|
||
| 1040 | content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
|
||
| 1041 | end |
||
| 1042 | |||
| 1043 | # Helper to render JSON in views |
||
| 1044 | def raw_json(arg) |
||
| 1045 | arg.to_json.to_s.gsub('/', '\/').html_safe
|
||
| 1046 | end |
||
| 1047 | |||
| 1048 | def back_url |
||
| 1049 | url = params[:back_url] |
||
| 1050 | if url.nil? && referer = request.env['HTTP_REFERER'] |
||
| 1051 | url = CGI.unescape(referer.to_s) |
||
| 1052 | end |
||
| 1053 | url |
||
| 1054 | end |
||
| 1055 | |||
| 1056 | def back_url_hidden_field_tag |
||
| 1057 | url = back_url |
||
| 1058 | hidden_field_tag('back_url', url, :id => nil) unless url.blank?
|
||
| 1059 | end |
||
| 1060 | |||
| 1061 | def check_all_links(form_name) |
||
| 1062 | link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
|
||
| 1063 | " | ".html_safe + |
||
| 1064 | link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
|
||
| 1065 | end |
||
| 1066 | |||
| 1067 | def progress_bar(pcts, options={})
|
||
| 1068 | pcts = [pcts, pcts] unless pcts.is_a?(Array) |
||
| 1069 | pcts = pcts.collect(&:round) |
||
| 1070 | pcts[1] = pcts[1] - pcts[0] |
||
| 1071 | pcts << (100 - pcts[1] - pcts[0]) |
||
| 1072 | width = options[:width] || '100px;' |
||
| 1073 | legend = options[:legend] || '' |
||
| 1074 | content_tag('table',
|
||
| 1075 | content_tag('tr',
|
||
| 1076 | (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
|
||
| 1077 | (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
|
||
| 1078 | (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
|
||
| 1079 | ), :class => 'progress', :style => "width: #{width};").html_safe +
|
||
| 1080 | content_tag('p', legend, :class => 'pourcent').html_safe
|
||
| 1081 | end |
||
| 1082 | |||
| 1083 | def checked_image(checked=true) |
||
| 1084 | if checked |
||
| 1085 | image_tag 'toggle_check.png' |
||
| 1086 | end |
||
| 1087 | end |
||
| 1088 | |||
| 1089 | def context_menu(url) |
||
| 1090 | unless @context_menu_included |
||
| 1091 | content_for :header_tags do |
||
| 1092 | javascript_include_tag('context_menu') +
|
||
| 1093 | stylesheet_link_tag('context_menu')
|
||
| 1094 | end |
||
| 1095 | if l(:direction) == 'rtl' |
||
| 1096 | content_for :header_tags do |
||
| 1097 | stylesheet_link_tag('context_menu_rtl')
|
||
| 1098 | end |
||
| 1099 | end |
||
| 1100 | @context_menu_included = true |
||
| 1101 | end |
||
| 1102 | javascript_tag "contextMenuInit('#{ url_for(url) }')"
|
||
| 1103 | end |
||
| 1104 | |||
| 1105 | def calendar_for(field_id) |
||
| 1106 | include_calendar_headers_tags |
||
| 1107 | javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
|
||
| 1108 | end |
||
| 1109 | |||
| 1110 | def include_calendar_headers_tags |
||
| 1111 | unless @calendar_headers_tags_included |
||
| 1112 | @calendar_headers_tags_included = true |
||
| 1113 | content_for :header_tags do |
||
| 1114 | start_of_week = Setting.start_of_week |
||
| 1115 | start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank? |
||
| 1116 | # Redmine uses 1..7 (monday..sunday) in settings and locales |
||
| 1117 | # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0 |
||
| 1118 | start_of_week = start_of_week.to_i % 7 |
||
| 1119 | |||
| 1120 | tags = javascript_tag( |
||
| 1121 | "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
|
||
| 1122 | "showOn: 'button', buttonImageOnly: true, buttonImage: '" + |
||
| 1123 | path_to_image('/images/calendar.png') +
|
||
| 1124 | "', showButtonPanel: true};") |
||
| 1125 | jquery_locale = l('jquery.locale', :default => current_language.to_s)
|
||
| 1126 | unless jquery_locale == 'en' |
||
| 1127 | tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js")
|
||
| 1128 | end |
||
| 1129 | tags |
||
| 1130 | end |
||
| 1131 | end |
||
| 1132 | end |
||
| 1133 | |||
| 1134 | # Overrides Rails' stylesheet_link_tag with themes and plugins support. |
||
| 1135 | # Examples: |
||
| 1136 | # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
|
||
| 1137 | # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
|
||
| 1138 | # |
||
| 1139 | def stylesheet_link_tag(*sources) |
||
| 1140 | options = sources.last.is_a?(Hash) ? sources.pop : {}
|
||
| 1141 | plugin = options.delete(:plugin) |
||
| 1142 | sources = sources.map do |source| |
||
| 1143 | if plugin |
||
| 1144 | "/plugin_assets/#{plugin}/stylesheets/#{source}"
|
||
| 1145 | elsif current_theme && current_theme.stylesheets.include?(source) |
||
| 1146 | current_theme.stylesheet_path(source) |
||
| 1147 | else |
||
| 1148 | source |
||
| 1149 | end |
||
| 1150 | end |
||
| 1151 | super sources, options |
||
| 1152 | end |
||
| 1153 | |||
| 1154 | # Overrides Rails' image_tag with themes and plugins support. |
||
| 1155 | # Examples: |
||
| 1156 | # image_tag('image.png') # => picks image.png from the current theme or defaults
|
||
| 1157 | # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
|
||
| 1158 | # |
||
| 1159 | def image_tag(source, options={})
|
||
| 1160 | if plugin = options.delete(:plugin) |
||
| 1161 | source = "/plugin_assets/#{plugin}/images/#{source}"
|
||
| 1162 | elsif current_theme && current_theme.images.include?(source) |
||
| 1163 | source = current_theme.image_path(source) |
||
| 1164 | end |
||
| 1165 | super source, options |
||
| 1166 | end |
||
| 1167 | |||
| 1168 | # Overrides Rails' javascript_include_tag with plugins support |
||
| 1169 | # Examples: |
||
| 1170 | # javascript_include_tag('scripts') # => picks scripts.js from defaults
|
||
| 1171 | # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
|
||
| 1172 | # |
||
| 1173 | def javascript_include_tag(*sources) |
||
| 1174 | options = sources.last.is_a?(Hash) ? sources.pop : {}
|
||
| 1175 | if plugin = options.delete(:plugin) |
||
| 1176 | sources = sources.map do |source| |
||
| 1177 | if plugin |
||
| 1178 | "/plugin_assets/#{plugin}/javascripts/#{source}"
|
||
| 1179 | else |
||
| 1180 | source |
||
| 1181 | end |
||
| 1182 | end |
||
| 1183 | end |
||
| 1184 | super sources, options |
||
| 1185 | end |
||
| 1186 | |||
| 1187 | def content_for(name, content = nil, &block) |
||
| 1188 | @has_content ||= {}
|
||
| 1189 | @has_content[name] = true |
||
| 1190 | super(name, content, &block) |
||
| 1191 | end |
||
| 1192 | |||
| 1193 | def has_content?(name) |
||
| 1194 | (@has_content && @has_content[name]) || false |
||
| 1195 | end |
||
| 1196 | |||
| 1197 | def sidebar_content? |
||
| 1198 | has_content?(:sidebar) || view_layouts_base_sidebar_hook_response.present? |
||
| 1199 | end |
||
| 1200 | |||
| 1201 | def view_layouts_base_sidebar_hook_response |
||
| 1202 | @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar) |
||
| 1203 | end |
||
| 1204 | |||
| 1205 | def email_delivery_enabled? |
||
| 1206 | !!ActionMailer::Base.perform_deliveries |
||
| 1207 | end |
||
| 1208 | |||
| 1209 | # Returns the avatar image tag for the given +user+ if avatars are enabled |
||
| 1210 | # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>') |
||
| 1211 | def avatar(user, options = { })
|
||
| 1212 | if Setting.gravatar_enabled? |
||
| 1213 | options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
|
||
| 1214 | email = nil |
||
| 1215 | if user.respond_to?(:mail) |
||
| 1216 | email = user.mail |
||
| 1217 | elsif user.to_s =~ %r{<(.+?)>}
|
||
| 1218 | email = $1 |
||
| 1219 | end |
||
| 1220 | return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil |
||
| 1221 | else |
||
| 1222 | '' |
||
| 1223 | end |
||
| 1224 | end |
||
| 1225 | |||
| 1226 | def sanitize_anchor_name(anchor) |
||
| 1227 | if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java' |
||
| 1228 | anchor.gsub(%r{[^\p{Word}\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
|
||
| 1229 | else |
||
| 1230 | # TODO: remove when ruby1.8 is no longer supported |
||
| 1231 | anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
|
||
| 1232 | end |
||
| 1233 | end |
||
| 1234 | |||
| 1235 | # Returns the javascript tags that are included in the html layout head |
||
| 1236 | def javascript_heads |
||
| 1237 | tags = javascript_include_tag('jquery-1.7.2-ui-1.8.21-ujs-2.0.3', 'application')
|
||
| 1238 | unless User.current.pref.warn_on_leaving_unsaved == '0' |
||
| 1239 | tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
|
||
| 1240 | end |
||
| 1241 | tags |
||
| 1242 | end |
||
| 1243 | |||
| 1244 | def favicon |
||
| 1245 | "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
|
||
| 1246 | end |
||
| 1247 | |||
| 1248 | def robot_exclusion_tag |
||
| 1249 | '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe |
||
| 1250 | end |
||
| 1251 | |||
| 1252 | # Returns true if arg is expected in the API response |
||
| 1253 | def include_in_api_response?(arg) |
||
| 1254 | unless @included_in_api_response |
||
| 1255 | param = params[:include] |
||
| 1256 | @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
|
||
| 1257 | @included_in_api_response.collect!(&:strip) |
||
| 1258 | end |
||
| 1259 | @included_in_api_response.include?(arg.to_s) |
||
| 1260 | end |
||
| 1261 | |||
| 1262 | # Returns options or nil if nometa param or X-Redmine-Nometa header |
||
| 1263 | # was set in the request |
||
| 1264 | def api_meta(options) |
||
| 1265 | if params[:nometa].present? || request.headers['X-Redmine-Nometa'] |
||
| 1266 | # compatibility mode for activeresource clients that raise |
||
| 1267 | # an error when unserializing an array with attributes |
||
| 1268 | nil |
||
| 1269 | else |
||
| 1270 | options |
||
| 1271 | end |
||
| 1272 | end |
||
| 1273 | |||
| 1274 | private |
||
| 1275 | |||
| 1276 | def wiki_helper |
||
| 1277 | helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting) |
||
| 1278 | extend helper |
||
| 1279 | return self |
||
| 1280 | end |
||
| 1281 | |||
| 1282 | def link_to_content_update(text, url_params = {}, html_options = {})
|
||
| 1283 | link_to(text, url_params, html_options) |
||
| 1284 | end |
||
| 1285 | end |