To check out this repository please hg clone the following URL, or open the URL using EasyMercurial or your preferred Mercurial client.
root / app / helpers / application_helper.rb @ 1026:b42553f6df71
History | View | Annotate | Download (40.1 KB)
| 1 | 909:cbb26bc654de | Chris | # encoding: utf-8
|
|---|---|---|---|
| 2 | #
|
||
| 3 | 37:94944d00e43c | chris | # Redmine - project management software
|
| 4 | 441:cbce1fd3b1b7 | Chris | # Copyright (C) 2006-2011 Jean-Philippe Lang
|
| 5 | 0:513646585e45 | Chris | #
|
| 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 | 22:40f7cfd4df19 | chris | #
|
| 38 | # @param [String] name Anchor text (passed to link_to)
|
||
| 39 | 37:94944d00e43c | chris | # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
|
| 40 | 22:40f7cfd4df19 | chris | # @param [optional, Hash] html_options Options passed to link_to
|
| 41 | # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
|
||
| 42 | 0:513646585e45 | Chris | def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference) |
| 43 | 37:94944d00e43c | chris | link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action]) |
| 44 | 0:513646585e45 | Chris | end
|
| 45 | |||
| 46 | # Display a link to remote if user is authorized
|
||
| 47 | def link_to_remote_if_authorized(name, options = {}, html_options = nil) |
||
| 48 | url = options[:url] || {}
|
||
| 49 | link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action]) |
||
| 50 | end
|
||
| 51 | |||
| 52 | # Displays a link to user's account page if active
|
||
| 53 | def link_to_user(user, options={}) |
||
| 54 | if user.is_a?(User) |
||
| 55 | name = h(user.name(options[:format]))
|
||
| 56 | if user.active?
|
||
| 57 | 140:4272e09f4b5f | chris | link_to(name, :controller => 'users', :action => 'show', :id => user) |
| 58 | 0:513646585e45 | Chris | else
|
| 59 | name |
||
| 60 | end
|
||
| 61 | else
|
||
| 62 | h(user.to_s) |
||
| 63 | end
|
||
| 64 | end
|
||
| 65 | |||
| 66 | # Displays a link to +issue+ with its subject.
|
||
| 67 | # Examples:
|
||
| 68 | 441:cbce1fd3b1b7 | Chris | #
|
| 69 | 0:513646585e45 | Chris | # link_to_issue(issue) # => Defect #6: This is the subject
|
| 70 | # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
|
||
| 71 | # link_to_issue(issue, :subject => false) # => Defect #6
|
||
| 72 | # link_to_issue(issue, :project => true) # => Foo - Defect #6
|
||
| 73 | #
|
||
| 74 | def link_to_issue(issue, options={}) |
||
| 75 | title = nil
|
||
| 76 | subject = nil
|
||
| 77 | if options[:subject] == false |
||
| 78 | title = truncate(issue.subject, :length => 60) |
||
| 79 | else
|
||
| 80 | subject = issue.subject |
||
| 81 | if options[:truncate] |
||
| 82 | subject = truncate(subject, :length => options[:truncate]) |
||
| 83 | end
|
||
| 84 | end
|
||
| 85 | 909:cbb26bc654de | Chris | s = link_to "#{h(issue.tracker)} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue}, |
| 86 | 0:513646585e45 | Chris | :class => issue.css_classes,
|
| 87 | :title => title
|
||
| 88 | s << ": #{h subject}" if subject |
||
| 89 | s = "#{h issue.project} - " + s if options[:project] |
||
| 90 | s |
||
| 91 | end
|
||
| 92 | |||
| 93 | # Generates a link to an attachment.
|
||
| 94 | # Options:
|
||
| 95 | # * :text - Link text (default to attachment filename)
|
||
| 96 | # * :download - Force download (default: false)
|
||
| 97 | def link_to_attachment(attachment, options={}) |
||
| 98 | text = options.delete(:text) || attachment.filename
|
||
| 99 | action = options.delete(:download) ? 'download' : 'show' |
||
| 100 | 909:cbb26bc654de | Chris | link_to(h(text), |
| 101 | {:controller => 'attachments', :action => action,
|
||
| 102 | :id => attachment, :filename => attachment.filename }, |
||
| 103 | options) |
||
| 104 | 0:513646585e45 | Chris | end
|
| 105 | |||
| 106 | # Generates a link to a SCM revision
|
||
| 107 | # Options:
|
||
| 108 | # * :text - Link text (default to the formatted revision)
|
||
| 109 | def link_to_revision(revision, project, options={}) |
||
| 110 | text = options.delete(:text) || format_revision(revision)
|
||
| 111 | 119:8661b858af72 | Chris | rev = revision.respond_to?(:identifier) ? revision.identifier : revision
|
| 112 | 0:513646585e45 | Chris | |
| 113 | 909:cbb26bc654de | Chris | link_to(h(text), {:controller => 'repositories', :action => 'revision', :id => project, :rev => rev},
|
| 114 | 119:8661b858af72 | Chris | :title => l(:label_revision_id, format_revision(revision))) |
| 115 | 0:513646585e45 | Chris | end
|
| 116 | 441:cbce1fd3b1b7 | Chris | |
| 117 | 210:0579821a129a | Chris | # Generates a link to a message
|
| 118 | def link_to_message(message, options={}, html_options = nil) |
||
| 119 | link_to( |
||
| 120 | h(truncate(message.subject, :length => 60)), |
||
| 121 | { :controller => 'messages', :action => 'show',
|
||
| 122 | :board_id => message.board_id,
|
||
| 123 | :id => message.root,
|
||
| 124 | :r => (message.parent_id && message.id),
|
||
| 125 | :anchor => (message.parent_id ? "message-#{message.id}" : nil) |
||
| 126 | }.merge(options), |
||
| 127 | html_options |
||
| 128 | ) |
||
| 129 | end
|
||
| 130 | 0:513646585e45 | Chris | |
| 131 | 14:1d32c0a0efbf | Chris | # Generates a link to a project if active
|
| 132 | # Examples:
|
||
| 133 | 441:cbce1fd3b1b7 | Chris | #
|
| 134 | 14:1d32c0a0efbf | Chris | # link_to_project(project) # => link to the specified project overview
|
| 135 | # link_to_project(project, :action=>'settings') # => link to project settings
|
||
| 136 | # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
|
||
| 137 | # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
|
||
| 138 | #
|
||
| 139 | def link_to_project(project, options={}, html_options = nil) |
||
| 140 | if project.active?
|
||
| 141 | url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
|
||
| 142 | link_to(h(project), url, html_options) |
||
| 143 | else
|
||
| 144 | h(project) |
||
| 145 | end
|
||
| 146 | end
|
||
| 147 | |||
| 148 | 0:513646585e45 | Chris | def toggle_link(name, id, options={}) |
| 149 | onclick = "Element.toggle('#{id}'); "
|
||
| 150 | onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ") |
||
| 151 | onclick << "return false;"
|
||
| 152 | link_to(name, "#", :onclick => onclick) |
||
| 153 | end
|
||
| 154 | |||
| 155 | def image_to_function(name, function, html_options = {}) |
||
| 156 | html_options.symbolize_keys! |
||
| 157 | tag(:input, html_options.merge({
|
||
| 158 | :type => "image", :src => image_path(name), |
||
| 159 | :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};" |
||
| 160 | })) |
||
| 161 | end
|
||
| 162 | |||
| 163 | def prompt_to_remote(name, text, param, url, html_options = {}) |
||
| 164 | html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;" |
||
| 165 | link_to name, {}, html_options
|
||
| 166 | end
|
||
| 167 | 441:cbce1fd3b1b7 | Chris | |
| 168 | 0:513646585e45 | Chris | def format_activity_title(text) |
| 169 | h(truncate_single_line(text, :length => 100)) |
||
| 170 | end
|
||
| 171 | 441:cbce1fd3b1b7 | Chris | |
| 172 | 0:513646585e45 | Chris | def format_activity_day(date) |
| 173 | date == Date.today ? l(:label_today).titleize : format_date(date) |
||
| 174 | end
|
||
| 175 | 441:cbce1fd3b1b7 | Chris | |
| 176 | 0:513646585e45 | Chris | def format_activity_description(text) |
| 177 | h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />") |
||
| 178 | end
|
||
| 179 | |||
| 180 | def format_version_name(version) |
||
| 181 | if version.project == @project |
||
| 182 | h(version) |
||
| 183 | else
|
||
| 184 | h("#{version.project} - #{version}")
|
||
| 185 | end
|
||
| 186 | end
|
||
| 187 | 441:cbce1fd3b1b7 | Chris | |
| 188 | 0:513646585e45 | Chris | def due_date_distance_in_words(date) |
| 189 | if date
|
||
| 190 | l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date)) |
||
| 191 | end
|
||
| 192 | end
|
||
| 193 | |||
| 194 | 441:cbce1fd3b1b7 | Chris | def render_page_hierarchy(pages, node=nil, options={}) |
| 195 | 0:513646585e45 | Chris | content = ''
|
| 196 | if pages[node]
|
||
| 197 | content << "<ul class=\"pages-hierarchy\">\n"
|
||
| 198 | pages[node].each do |page|
|
||
| 199 | content << "<li>"
|
||
| 200 | 37:94944d00e43c | chris | content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
|
| 201 | 441:cbce1fd3b1b7 | Chris | :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil)) |
| 202 | content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id] |
||
| 203 | 0:513646585e45 | Chris | content << "</li>\n"
|
| 204 | end
|
||
| 205 | content << "</ul>\n"
|
||
| 206 | end
|
||
| 207 | 909:cbb26bc654de | Chris | content.html_safe |
| 208 | 0:513646585e45 | Chris | end
|
| 209 | 441:cbce1fd3b1b7 | Chris | |
| 210 | 0:513646585e45 | Chris | # Renders flash messages
|
| 211 | def render_flash_messages |
||
| 212 | s = ''
|
||
| 213 | flash.each do |k,v|
|
||
| 214 | s << content_tag('div', v, :class => "flash #{k}") |
||
| 215 | end
|
||
| 216 | 909:cbb26bc654de | Chris | s.html_safe |
| 217 | 0:513646585e45 | Chris | end
|
| 218 | 441:cbce1fd3b1b7 | Chris | |
| 219 | 0:513646585e45 | Chris | # Renders tabs and their content
|
| 220 | def render_tabs(tabs) |
||
| 221 | if tabs.any?
|
||
| 222 | render :partial => 'common/tabs', :locals => {:tabs => tabs} |
||
| 223 | else
|
||
| 224 | content_tag 'p', l(:label_no_data), :class => "nodata" |
||
| 225 | end
|
||
| 226 | end
|
||
| 227 | 441:cbce1fd3b1b7 | Chris | |
| 228 | 0:513646585e45 | Chris | # Renders the project quick-jump box
|
| 229 | def render_project_jump_box |
||
| 230 | 441:cbce1fd3b1b7 | Chris | return unless User.current.logged? |
| 231 | projects = User.current.memberships.collect(&:project).compact.uniq |
||
| 232 | 0:513646585e45 | Chris | if projects.any?
|
| 233 | s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
|
||
| 234 | "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
|
||
| 235 | '<option value="" disabled="disabled">---</option>'
|
||
| 236 | s << project_tree_options_for_select(projects, :selected => @project) do |p| |
||
| 237 | { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
|
||
| 238 | end
|
||
| 239 | s << '</select>'
|
||
| 240 | 909:cbb26bc654de | Chris | s.html_safe |
| 241 | 0:513646585e45 | Chris | end
|
| 242 | end
|
||
| 243 | 441:cbce1fd3b1b7 | Chris | |
| 244 | 0:513646585e45 | Chris | def project_tree_options_for_select(projects, options = {}) |
| 245 | s = ''
|
||
| 246 | project_tree(projects) do |project, level|
|
||
| 247 | name_prefix = (level > 0 ? (' ' * 2 * level + '» ') : '') |
||
| 248 | tag_options = {:value => project.id}
|
||
| 249 | if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project)) |
||
| 250 | tag_options[:selected] = 'selected' |
||
| 251 | else
|
||
| 252 | tag_options[:selected] = nil |
||
| 253 | end
|
||
| 254 | tag_options.merge!(yield(project)) if block_given? |
||
| 255 | s << content_tag('option', name_prefix + h(project), tag_options)
|
||
| 256 | end
|
||
| 257 | 909:cbb26bc654de | Chris | s.html_safe |
| 258 | 0:513646585e45 | Chris | end
|
| 259 | 441:cbce1fd3b1b7 | Chris | |
| 260 | 0:513646585e45 | Chris | # Yields the given block for each project with its level in the tree
|
| 261 | 37:94944d00e43c | chris | #
|
| 262 | # Wrapper for Project#project_tree
|
||
| 263 | 0:513646585e45 | Chris | def project_tree(projects, &block) |
| 264 | 37:94944d00e43c | chris | Project.project_tree(projects, &block)
|
| 265 | 0:513646585e45 | Chris | end
|
| 266 | 441:cbce1fd3b1b7 | Chris | |
| 267 | 0:513646585e45 | Chris | def project_nested_ul(projects, &block) |
| 268 | s = ''
|
||
| 269 | if projects.any?
|
||
| 270 | ancestors = [] |
||
| 271 | projects.sort_by(&:lft).each do |project| |
||
| 272 | if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
|
||
| 273 | s << "<ul>\n"
|
||
| 274 | else
|
||
| 275 | ancestors.pop |
||
| 276 | s << "</li>"
|
||
| 277 | 441:cbce1fd3b1b7 | Chris | while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
|
| 278 | 0:513646585e45 | Chris | ancestors.pop |
| 279 | s << "</ul></li>\n"
|
||
| 280 | end
|
||
| 281 | end
|
||
| 282 | s << "<li>"
|
||
| 283 | s << yield(project).to_s
|
||
| 284 | ancestors << project |
||
| 285 | end
|
||
| 286 | s << ("</li></ul>\n" * ancestors.size)
|
||
| 287 | end
|
||
| 288 | 909:cbb26bc654de | Chris | s.html_safe |
| 289 | 0:513646585e45 | Chris | end
|
| 290 | 441:cbce1fd3b1b7 | Chris | |
| 291 | 0:513646585e45 | Chris | def principals_check_box_tags(name, principals) |
| 292 | s = ''
|
||
| 293 | principals.sort.each do |principal|
|
||
| 294 | 948:83866d58f2dd | luis | |
| 295 | if principal.type == "User": |
||
| 296 | s << "<label>#{ check_box_tag name, principal.id, false } #{link_to_user principal}</label>\n"
|
||
| 297 | else
|
||
| 298 | s << "<label>#{ check_box_tag name, principal.id, false } #{h principal} (Group)</label>\n"
|
||
| 299 | end
|
||
| 300 | |||
| 301 | 0:513646585e45 | Chris | end
|
| 302 | 909:cbb26bc654de | Chris | s.html_safe |
| 303 | end
|
||
| 304 | |||
| 305 | # Returns a string for users/groups option tags
|
||
| 306 | def principals_options_for_select(collection, selected=nil) |
||
| 307 | s = ''
|
||
| 308 | groups = ''
|
||
| 309 | collection.sort.each do |element|
|
||
| 310 | selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) |
||
| 311 | (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>) |
||
| 312 | end
|
||
| 313 | unless groups.empty?
|
||
| 314 | s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
|
||
| 315 | end
|
||
| 316 | 441:cbce1fd3b1b7 | Chris | s |
| 317 | 0:513646585e45 | Chris | end
|
| 318 | |||
| 319 | # Truncates and returns the string as a single line
|
||
| 320 | def truncate_single_line(string, *args) |
||
| 321 | truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ') |
||
| 322 | end
|
||
| 323 | 441:cbce1fd3b1b7 | Chris | |
| 324 | 0:513646585e45 | Chris | # Truncates at line break after 250 characters or options[:length]
|
| 325 | def truncate_lines(string, options={}) |
||
| 326 | length = options[:length] || 250 |
||
| 327 | if string.to_s =~ /\A(.{#{length}}.*?)$/m |
||
| 328 | "#{$1}..."
|
||
| 329 | else
|
||
| 330 | string |
||
| 331 | end
|
||
| 332 | end
|
||
| 333 | |||
| 334 | def html_hours(text) |
||
| 335 | 909:cbb26bc654de | Chris | text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe |
| 336 | 0:513646585e45 | Chris | end
|
| 337 | |||
| 338 | def authoring(created, author, options={}) |
||
| 339 | 909:cbb26bc654de | Chris | l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe |
| 340 | 0:513646585e45 | Chris | end
|
| 341 | 441:cbce1fd3b1b7 | Chris | |
| 342 | 0:513646585e45 | Chris | def time_tag(time) |
| 343 | text = distance_of_time_in_words(Time.now, time)
|
||
| 344 | if @project |
||
| 345 | 22:40f7cfd4df19 | chris | link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
|
| 346 | 0:513646585e45 | Chris | else
|
| 347 | content_tag('acronym', text, :title => format_time(time)) |
||
| 348 | end
|
||
| 349 | end
|
||
| 350 | |||
| 351 | def syntax_highlight(name, content) |
||
| 352 | Redmine::SyntaxHighlighting.highlight_by_filename(content, name) |
||
| 353 | end
|
||
| 354 | |||
| 355 | def to_path_param(path) |
||
| 356 | path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
|
||
| 357 | end
|
||
| 358 | |||
| 359 | def pagination_links_full(paginator, count=nil, options={}) |
||
| 360 | page_param = options.delete(:page_param) || :page |
||
| 361 | per_page_links = options.delete(:per_page_links)
|
||
| 362 | url_param = params.dup |
||
| 363 | |||
| 364 | html = ''
|
||
| 365 | if paginator.current.previous
|
||
| 366 | 909:cbb26bc654de | Chris | # \xc2\xab(utf-8) = «
|
| 367 | html << link_to_content_update( |
||
| 368 | "\xc2\xab " + l(:label_previous), |
||
| 369 | url_param.merge(page_param => paginator.current.previous)) + ' '
|
||
| 370 | 0:513646585e45 | Chris | end
|
| 371 | |||
| 372 | html << (pagination_links_each(paginator, options) do |n|
|
||
| 373 | 441:cbce1fd3b1b7 | Chris | link_to_content_update(n.to_s, url_param.merge(page_param => n)) |
| 374 | 0:513646585e45 | Chris | end || '') |
| 375 | 441:cbce1fd3b1b7 | Chris | |
| 376 | 0:513646585e45 | Chris | if paginator.current.next
|
| 377 | 909:cbb26bc654de | Chris | # \xc2\xbb(utf-8) = »
|
| 378 | html << ' ' + link_to_content_update(
|
||
| 379 | (l(:label_next) + " \xc2\xbb"), |
||
| 380 | url_param.merge(page_param => paginator.current.next)) |
||
| 381 | 0:513646585e45 | Chris | end
|
| 382 | |||
| 383 | unless count.nil?
|
||
| 384 | html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
|
||
| 385 | if per_page_links != false && links = per_page_links(paginator.items_per_page) |
||
| 386 | html << " | #{links}"
|
||
| 387 | end
|
||
| 388 | end
|
||
| 389 | |||
| 390 | 909:cbb26bc654de | Chris | html.html_safe |
| 391 | 0:513646585e45 | Chris | end
|
| 392 | 441:cbce1fd3b1b7 | Chris | |
| 393 | 0:513646585e45 | Chris | def per_page_links(selected=nil) |
| 394 | links = Setting.per_page_options_array.collect do |n| |
||
| 395 | 441:cbce1fd3b1b7 | Chris | n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
|
| 396 | 0:513646585e45 | Chris | end
|
| 397 | links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil |
||
| 398 | end
|
||
| 399 | 441:cbce1fd3b1b7 | Chris | |
| 400 | 909:cbb26bc654de | Chris | def reorder_links(name, url, method = :post) |
| 401 | link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), |
||
| 402 | url.merge({"#{name}[move_to]" => 'highest'}),
|
||
| 403 | :method => method, :title => l(:label_sort_highest)) + |
||
| 404 | link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), |
||
| 405 | url.merge({"#{name}[move_to]" => 'higher'}),
|
||
| 406 | :method => method, :title => l(:label_sort_higher)) + |
||
| 407 | link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), |
||
| 408 | url.merge({"#{name}[move_to]" => 'lower'}),
|
||
| 409 | :method => method, :title => l(:label_sort_lower)) + |
||
| 410 | link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), |
||
| 411 | url.merge({"#{name}[move_to]" => 'lowest'}),
|
||
| 412 | :method => method, :title => l(:label_sort_lowest)) |
||
| 413 | 0:513646585e45 | Chris | end
|
| 414 | |||
| 415 | def breadcrumb(*args) |
||
| 416 | elements = args.flatten |
||
| 417 | 909:cbb26bc654de | Chris | elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil |
| 418 | 0:513646585e45 | Chris | end
|
| 419 | 441:cbce1fd3b1b7 | Chris | |
| 420 | 0:513646585e45 | Chris | def other_formats_links(&block) |
| 421 | 909:cbb26bc654de | Chris | concat('<p class="other-formats">'.html_safe + l(:label_export_to)) |
| 422 | 0:513646585e45 | Chris | yield Redmine::Views::OtherFormatsBuilder.new(self) |
| 423 | 909:cbb26bc654de | Chris | concat('</p>'.html_safe)
|
| 424 | 0:513646585e45 | Chris | end
|
| 425 | 441:cbce1fd3b1b7 | Chris | |
| 426 | 0:513646585e45 | Chris | def page_header_title |
| 427 | if @project.nil? || @project.new_record? |
||
| 428 | 144:09910262eb0b | luisf | a = [h(Setting.app_title), ''] |
| 429 | |||
| 430 | 0:513646585e45 | Chris | else
|
| 431 | 144:09910262eb0b | luisf | pname = [] |
| 432 | 0:513646585e45 | Chris | b = [] |
| 433 | 441:cbce1fd3b1b7 | Chris | ancestors = (@project.root? ? [] : @project.ancestors.visible.all) |
| 434 | 0:513646585e45 | Chris | if ancestors.any?
|
| 435 | root = ancestors.shift |
||
| 436 | 14:1d32c0a0efbf | Chris | b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
|
| 437 | 0:513646585e45 | Chris | if ancestors.size > 2 |
| 438 | 144:09910262eb0b | luisf | b << '…'
|
| 439 | 0:513646585e45 | Chris | ancestors = ancestors[-2, 2] |
| 440 | end
|
||
| 441 | 14:1d32c0a0efbf | Chris | b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
|
| 442 | 144:09910262eb0b | luisf | b = b.join(' » ')
|
| 443 | b << (' »')
|
||
| 444 | 0:513646585e45 | Chris | end
|
| 445 | 144:09910262eb0b | luisf | |
| 446 | pname << h(@project)
|
||
| 447 | |||
| 448 | a = [pname, b] |
||
| 449 | |||
| 450 | 0:513646585e45 | Chris | end
|
| 451 | end
|
||
| 452 | |||
| 453 | def html_title(*args) |
||
| 454 | if args.empty?
|
||
| 455 | 909:cbb26bc654de | Chris | title = @html_title || []
|
| 456 | 0:513646585e45 | Chris | title << @project.name if @project |
| 457 | 909:cbb26bc654de | Chris | title << Setting.app_title unless Setting.app_title == title.last |
| 458 | 0:513646585e45 | Chris | title.select {|t| !t.blank? }.join(' - ')
|
| 459 | else
|
||
| 460 | @html_title ||= []
|
||
| 461 | @html_title += args
|
||
| 462 | end
|
||
| 463 | end
|
||
| 464 | |||
| 465 | 14:1d32c0a0efbf | Chris | # Returns the theme, controller name, and action as css classes for the
|
| 466 | # HTML body.
|
||
| 467 | def body_css_classes |
||
| 468 | css = [] |
||
| 469 | if theme = Redmine::Themes.theme(Setting.ui_theme) |
||
| 470 | css << 'theme-' + theme.name
|
||
| 471 | end
|
||
| 472 | |||
| 473 | css << 'controller-' + params[:controller] |
||
| 474 | css << 'action-' + params[:action] |
||
| 475 | css.join(' ')
|
||
| 476 | end
|
||
| 477 | |||
| 478 | 0:513646585e45 | Chris | def accesskey(s) |
| 479 | Redmine::AccessKeys.key_for s |
||
| 480 | end
|
||
| 481 | |||
| 482 | # Formats text according to system settings.
|
||
| 483 | # 2 ways to call this method:
|
||
| 484 | # * with a String: textilizable(text, options)
|
||
| 485 | # * with an object and one of its attribute: textilizable(issue, :description, options)
|
||
| 486 | def textilizable(*args) |
||
| 487 | options = args.last.is_a?(Hash) ? args.pop : {}
|
||
| 488 | case args.size
|
||
| 489 | when 1 |
||
| 490 | obj = options[:object]
|
||
| 491 | text = args.shift |
||
| 492 | when 2 |
||
| 493 | obj = args.shift |
||
| 494 | attr = args.shift |
||
| 495 | text = obj.send(attr).to_s |
||
| 496 | else
|
||
| 497 | raise ArgumentError, 'invalid arguments to textilizable' |
||
| 498 | end
|
||
| 499 | return '' if text.blank? |
||
| 500 | project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil) |
||
| 501 | only_path = options.delete(:only_path) == false ? false : true |
||
| 502 | |||
| 503 | 909:cbb26bc654de | Chris | text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) |
| 504 | 441:cbce1fd3b1b7 | Chris | |
| 505 | 119:8661b858af72 | Chris | @parsed_headings = []
|
| 506 | 929:5f33065ddc4b | Chris | @heading_anchors = {}
|
| 507 | 909:cbb26bc654de | Chris | @current_section = 0 if options[:edit_section_links] |
| 508 | 929:5f33065ddc4b | Chris | |
| 509 | parse_sections(text, project, obj, attr, only_path, options) |
||
| 510 | 119:8661b858af72 | Chris | text = parse_non_pre_blocks(text) do |text|
|
| 511 | 929:5f33065ddc4b | Chris | [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_macros].each do |method_name| |
| 512 | 0:513646585e45 | Chris | send method_name, text, project, obj, attr, only_path, options |
| 513 | end
|
||
| 514 | end
|
||
| 515 | 929:5f33065ddc4b | Chris | parse_headings(text, project, obj, attr, only_path, options) |
| 516 | 441:cbce1fd3b1b7 | Chris | |
| 517 | 119:8661b858af72 | Chris | if @parsed_headings.any? |
| 518 | replace_toc(text, @parsed_headings)
|
||
| 519 | end
|
||
| 520 | 441:cbce1fd3b1b7 | Chris | |
| 521 | 119:8661b858af72 | Chris | text |
| 522 | 0:513646585e45 | Chris | end
|
| 523 | 441:cbce1fd3b1b7 | Chris | |
| 524 | 0:513646585e45 | Chris | def parse_non_pre_blocks(text) |
| 525 | s = StringScanner.new(text)
|
||
| 526 | tags = [] |
||
| 527 | parsed = ''
|
||
| 528 | while !s.eos?
|
||
| 529 | s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
|
||
| 530 | text, full_tag, closing, tag = s[1], s[2], s[3], s[4] |
||
| 531 | if tags.empty?
|
||
| 532 | yield text
|
||
| 533 | end
|
||
| 534 | parsed << text |
||
| 535 | if tag
|
||
| 536 | if closing
|
||
| 537 | if tags.last == tag.downcase
|
||
| 538 | tags.pop |
||
| 539 | end
|
||
| 540 | else
|
||
| 541 | tags << tag.downcase |
||
| 542 | end
|
||
| 543 | parsed << full_tag |
||
| 544 | end
|
||
| 545 | end
|
||
| 546 | # Close any non closing tags
|
||
| 547 | while tag = tags.pop
|
||
| 548 | parsed << "</#{tag}>"
|
||
| 549 | end
|
||
| 550 | 909:cbb26bc654de | Chris | parsed.html_safe |
| 551 | 0:513646585e45 | Chris | end
|
| 552 | 441:cbce1fd3b1b7 | Chris | |
| 553 | 0:513646585e45 | Chris | def parse_inline_attachments(text, project, obj, attr, only_path, options) |
| 554 | # when using an image link, try to use an attachment, if possible
|
||
| 555 | if options[:attachments] || (obj && obj.respond_to?(:attachments)) |
||
| 556 | 909:cbb26bc654de | Chris | attachments = options[:attachments] || obj.attachments
|
| 557 | text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m| |
||
| 558 | 441:cbce1fd3b1b7 | Chris | filename, ext, alt, alttext = $1.downcase, $2, $3, $4 |
| 559 | 0:513646585e45 | Chris | # search for the picture in attachments
|
| 560 | 909:cbb26bc654de | Chris | if found = Attachment.latest_attach(attachments, filename) |
| 561 | image_url = url_for :only_path => only_path, :controller => 'attachments', |
||
| 562 | :action => 'download', :id => found |
||
| 563 | 0:513646585e45 | Chris | desc = found.description.to_s.gsub('"', '') |
| 564 | if !desc.blank? && alttext.blank?
|
||
| 565 | alt = " title=\"#{desc}\" alt=\"#{desc}\""
|
||
| 566 | end
|
||
| 567 | 909:cbb26bc654de | Chris | "src=\"#{image_url}\"#{alt}".html_safe
|
| 568 | 0:513646585e45 | Chris | else
|
| 569 | 909:cbb26bc654de | Chris | m.html_safe |
| 570 | 0:513646585e45 | Chris | end
|
| 571 | end
|
||
| 572 | end
|
||
| 573 | end
|
||
| 574 | |||
| 575 | # Wiki links
|
||
| 576 | #
|
||
| 577 | # Examples:
|
||
| 578 | # [[mypage]]
|
||
| 579 | # [[mypage|mytext]]
|
||
| 580 | # wiki links can refer other project wikis, using project name or identifier:
|
||
| 581 | # [[project:]] -> wiki starting page
|
||
| 582 | # [[project:|mytext]]
|
||
| 583 | # [[project:mypage]]
|
||
| 584 | # [[project:mypage|mytext]]
|
||
| 585 | def parse_wiki_links(text, project, obj, attr, only_path, options) |
||
| 586 | text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m| |
||
| 587 | link_project = project |
||
| 588 | esc, all, page, title = $1, $2, $3, $5 |
||
| 589 | if esc.nil?
|
||
| 590 | if page =~ /^([^\:]+)\:(.*)$/ |
||
| 591 | 37:94944d00e43c | chris | link_project = Project.find_by_identifier($1) || Project.find_by_name($1) |
| 592 | 0:513646585e45 | Chris | page = $2
|
| 593 | title ||= $1 if page.blank? |
||
| 594 | end
|
||
| 595 | |||
| 596 | if link_project && link_project.wiki
|
||
| 597 | # extract anchor
|
||
| 598 | anchor = nil
|
||
| 599 | if page =~ /^(.+?)\#(.+)$/ |
||
| 600 | page, anchor = $1, $2 |
||
| 601 | end
|
||
| 602 | 909:cbb26bc654de | Chris | anchor = sanitize_anchor_name(anchor) if anchor.present?
|
| 603 | 0:513646585e45 | Chris | # check if page exists
|
| 604 | wiki_page = link_project.wiki.find_page(page) |
||
| 605 | 909:cbb26bc654de | Chris | url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page |
| 606 | "##{anchor}"
|
||
| 607 | else
|
||
| 608 | case options[:wiki_links] |
||
| 609 | when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '') |
||
| 610 | when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export |
||
| 611 | 0:513646585e45 | Chris | else
|
| 612 | 37:94944d00e43c | chris | wiki_page_id = page.present? ? Wiki.titleize(page) : nil |
| 613 | url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, :id => wiki_page_id, :anchor => anchor) |
||
| 614 | 0:513646585e45 | Chris | end
|
| 615 | 909:cbb26bc654de | Chris | end
|
| 616 | link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new'))) |
||
| 617 | 0:513646585e45 | Chris | else
|
| 618 | # project or wiki doesn't exist
|
||
| 619 | 909:cbb26bc654de | Chris | all.html_safe |
| 620 | 0:513646585e45 | Chris | end
|
| 621 | else
|
||
| 622 | 909:cbb26bc654de | Chris | all.html_safe |
| 623 | 0:513646585e45 | Chris | end
|
| 624 | end
|
||
| 625 | end
|
||
| 626 | 441:cbce1fd3b1b7 | Chris | |
| 627 | 0:513646585e45 | Chris | # Redmine links
|
| 628 | #
|
||
| 629 | # Examples:
|
||
| 630 | # Issues:
|
||
| 631 | # #52 -> Link to issue #52
|
||
| 632 | # Changesets:
|
||
| 633 | # r52 -> Link to revision 52
|
||
| 634 | # commit:a85130f -> Link to scmid starting with a85130f
|
||
| 635 | # Documents:
|
||
| 636 | # document#17 -> Link to document with id 17
|
||
| 637 | # document:Greetings -> Link to the document with title "Greetings"
|
||
| 638 | # document:"Some document" -> Link to the document with title "Some document"
|
||
| 639 | # Versions:
|
||
| 640 | # version#3 -> Link to version with id 3
|
||
| 641 | # version:1.0.0 -> Link to version named "1.0.0"
|
||
| 642 | # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
|
||
| 643 | # Attachments:
|
||
| 644 | # attachment:file.zip -> Link to the attachment of the current object named file.zip
|
||
| 645 | # Source files:
|
||
| 646 | # source:some/file -> Link to the file located at /some/file in the project's repository
|
||
| 647 | # source:some/file@52 -> Link to the file's revision 52
|
||
| 648 | # source:some/file#L120 -> Link to line 120 of the file
|
||
| 649 | # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
|
||
| 650 | # export:some/file -> Force the download of the file
|
||
| 651 | 210:0579821a129a | Chris | # Forum messages:
|
| 652 | 0:513646585e45 | Chris | # message#1218 -> Link to message with id 1218
|
| 653 | 210:0579821a129a | Chris | #
|
| 654 | # Links can refer other objects from other projects, using project identifier:
|
||
| 655 | # identifier:r52
|
||
| 656 | # identifier:document:"Some document"
|
||
| 657 | # identifier:version:1.0.0
|
||
| 658 | # identifier:source:some/file
|
||
| 659 | 0:513646585e45 | Chris | def parse_redmine_links(text, project, obj, attr, only_path, options) |
| 660 | 909:cbb26bc654de | Chris | 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| |
| 661 | 210:0579821a129a | Chris | leading, esc, project_prefix, project_identifier, prefix, sep, identifier = $1, $2, $3, $4, $5, $7 || $9, $8 || $10 |
| 662 | 0:513646585e45 | Chris | link = nil
|
| 663 | 210:0579821a129a | Chris | if project_identifier
|
| 664 | project = Project.visible.find_by_identifier(project_identifier)
|
||
| 665 | end
|
||
| 666 | 0:513646585e45 | Chris | if esc.nil?
|
| 667 | if prefix.nil? && sep == 'r' |
||
| 668 | 210:0579821a129a | Chris | # project.changesets.visible raises an SQL error because of a double join on repositories
|
| 669 | if project && project.repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(project.repository.id, identifier)) |
||
| 670 | 909:cbb26bc654de | Chris | link = link_to(h("#{project_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision}, |
| 671 | 0:513646585e45 | Chris | :class => 'changeset', |
| 672 | :title => truncate_single_line(changeset.comments, :length => 100)) |
||
| 673 | end
|
||
| 674 | elsif sep == '#' |
||
| 675 | oid = identifier.to_i |
||
| 676 | case prefix
|
||
| 677 | when nil |
||
| 678 | if issue = Issue.visible.find_by_id(oid, :include => :status) |
||
| 679 | link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid}, |
||
| 680 | :class => issue.css_classes,
|
||
| 681 | :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})") |
||
| 682 | end
|
||
| 683 | when 'document' |
||
| 684 | 210:0579821a129a | Chris | if document = Document.visible.find_by_id(oid) |
| 685 | 0:513646585e45 | Chris | link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
|
| 686 | :class => 'document' |
||
| 687 | end
|
||
| 688 | when 'version' |
||
| 689 | 210:0579821a129a | Chris | if version = Version.visible.find_by_id(oid) |
| 690 | 0:513646585e45 | Chris | link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
|
| 691 | :class => 'version' |
||
| 692 | end
|
||
| 693 | when 'message' |
||
| 694 | 210:0579821a129a | Chris | if message = Message.visible.find_by_id(oid, :include => :parent) |
| 695 | link = link_to_message(message, {:only_path => only_path}, :class => 'message')
|
||
| 696 | 0:513646585e45 | Chris | end
|
| 697 | 909:cbb26bc654de | Chris | when 'forum' |
| 698 | if board = Board.visible.find_by_id(oid) |
||
| 699 | link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
|
||
| 700 | :class => 'board' |
||
| 701 | end
|
||
| 702 | when 'news' |
||
| 703 | if news = News.visible.find_by_id(oid) |
||
| 704 | link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
|
||
| 705 | :class => 'news' |
||
| 706 | end
|
||
| 707 | 0:513646585e45 | Chris | when 'project' |
| 708 | if p = Project.visible.find_by_id(oid) |
||
| 709 | 14:1d32c0a0efbf | Chris | link = link_to_project(p, {:only_path => only_path}, :class => 'project')
|
| 710 | 0:513646585e45 | Chris | end
|
| 711 | end
|
||
| 712 | elsif sep == ':' |
||
| 713 | # removes the double quotes if any
|
||
| 714 | name = identifier.gsub(%r{^"(.*)"$}, "\\1") |
||
| 715 | case prefix
|
||
| 716 | when 'document' |
||
| 717 | 210:0579821a129a | Chris | if project && document = project.documents.visible.find_by_title(name)
|
| 718 | 0:513646585e45 | Chris | link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
|
| 719 | :class => 'document' |
||
| 720 | end
|
||
| 721 | when 'version' |
||
| 722 | 210:0579821a129a | Chris | if project && version = project.versions.visible.find_by_name(name)
|
| 723 | 0:513646585e45 | Chris | link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
|
| 724 | :class => 'version' |
||
| 725 | end
|
||
| 726 | 909:cbb26bc654de | Chris | when 'forum' |
| 727 | if project && board = project.boards.visible.find_by_name(name)
|
||
| 728 | link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
|
||
| 729 | :class => 'board' |
||
| 730 | end
|
||
| 731 | when 'news' |
||
| 732 | if project && news = project.news.visible.find_by_title(name)
|
||
| 733 | link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
|
||
| 734 | :class => 'news' |
||
| 735 | end
|
||
| 736 | 0:513646585e45 | Chris | when 'commit' |
| 737 | 210:0579821a129a | Chris | if project && project.repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", project.repository.id, "#{name}%"])) |
| 738 | link = link_to h("#{project_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.identifier}, |
||
| 739 | 0:513646585e45 | Chris | :class => 'changeset', |
| 740 | 909:cbb26bc654de | Chris | :title => truncate_single_line(h(changeset.comments), :length => 100) |
| 741 | 0:513646585e45 | Chris | end
|
| 742 | when 'source', 'export' |
||
| 743 | 210:0579821a129a | Chris | if project && project.repository && User.current.allowed_to?(:browse_repository, project) |
| 744 | 0:513646585e45 | Chris | name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
|
| 745 | path, rev, anchor = $1, $3, $5 |
||
| 746 | 210:0579821a129a | Chris | link = link_to h("#{project_prefix}#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project, |
| 747 | 0:513646585e45 | Chris | :path => to_path_param(path),
|
| 748 | :rev => rev,
|
||
| 749 | :anchor => anchor,
|
||
| 750 | :format => (prefix == 'export' ? 'raw' : nil)}, |
||
| 751 | :class => (prefix == 'export' ? 'source download' : 'source') |
||
| 752 | end
|
||
| 753 | when 'attachment' |
||
| 754 | attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil) |
||
| 755 | if attachments && attachment = attachments.detect {|a| a.filename == name }
|
||
| 756 | link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
|
||
| 757 | :class => 'attachment' |
||
| 758 | end
|
||
| 759 | when 'project' |
||
| 760 | if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}]) |
||
| 761 | 14:1d32c0a0efbf | Chris | link = link_to_project(p, {:only_path => only_path}, :class => 'project')
|
| 762 | 0:513646585e45 | Chris | end
|
| 763 | end
|
||
| 764 | end
|
||
| 765 | end
|
||
| 766 | 909:cbb26bc654de | Chris | (leading + (link || "#{project_prefix}#{prefix}#{sep}#{identifier}")).html_safe
|
| 767 | 0:513646585e45 | Chris | end
|
| 768 | end
|
||
| 769 | 441:cbce1fd3b1b7 | Chris | |
| 770 | 909:cbb26bc654de | Chris | HEADING_RE = /(<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>)/i unless const_defined?(:HEADING_RE) |
| 771 | |||
| 772 | def parse_sections(text, project, obj, attr, only_path, options) |
||
| 773 | return unless options[:edit_section_links] |
||
| 774 | text.gsub!(HEADING_RE) do |
||
| 775 | @current_section += 1 |
||
| 776 | if @current_section > 1 |
||
| 777 | content_tag('div',
|
||
| 778 | link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)), |
||
| 779 | :class => 'contextual', |
||
| 780 | :title => l(:button_edit_section)) + $1 |
||
| 781 | else
|
||
| 782 | $1
|
||
| 783 | end
|
||
| 784 | end
|
||
| 785 | end
|
||
| 786 | 441:cbce1fd3b1b7 | Chris | |
| 787 | 37:94944d00e43c | chris | # Headings and TOC
|
| 788 | 119:8661b858af72 | Chris | # Adds ids and links to headings unless options[:headings] is set to false
|
| 789 | 37:94944d00e43c | chris | def parse_headings(text, project, obj, attr, only_path, options) |
| 790 | 119:8661b858af72 | Chris | return if options[:headings] == false |
| 791 | 441:cbce1fd3b1b7 | Chris | |
| 792 | 37:94944d00e43c | chris | text.gsub!(HEADING_RE) do |
| 793 | 909:cbb26bc654de | Chris | level, attrs, content = $2.to_i, $3, $4 |
| 794 | 37:94944d00e43c | chris | item = strip_tags(content).strip |
| 795 | 909:cbb26bc654de | Chris | anchor = sanitize_anchor_name(item) |
| 796 | # used for single-file wiki export
|
||
| 797 | anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) |
||
| 798 | 929:5f33065ddc4b | Chris | @heading_anchors[anchor] ||= 0 |
| 799 | idx = (@heading_anchors[anchor] += 1) |
||
| 800 | if idx > 1 |
||
| 801 | anchor = "#{anchor}-#{idx}"
|
||
| 802 | end
|
||
| 803 | 119:8661b858af72 | Chris | @parsed_headings << [level, anchor, item]
|
| 804 | 441:cbce1fd3b1b7 | Chris | "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">¶</a></h#{level}>"
|
| 805 | 119:8661b858af72 | Chris | end
|
| 806 | end
|
||
| 807 | 441:cbce1fd3b1b7 | Chris | |
| 808 | 909:cbb26bc654de | Chris | MACROS_RE = / |
| 809 | (!)? # escaping
|
||
| 810 | (
|
||
| 811 | \{\{ # opening tag
|
||
| 812 | ([\w]+) # macro name
|
||
| 813 | (\(([^\}]*)\))? # optional arguments
|
||
| 814 | \}\} # closing tag
|
||
| 815 | )
|
||
| 816 | /x unless const_defined?(:MACROS_RE) |
||
| 817 | |||
| 818 | # Macros substitution
|
||
| 819 | def parse_macros(text, project, obj, attr, only_path, options) |
||
| 820 | text.gsub!(MACROS_RE) do |
||
| 821 | esc, all, macro = $1, $2, $3.downcase |
||
| 822 | args = ($5 || '').split(',').each(&:strip) |
||
| 823 | if esc.nil?
|
||
| 824 | begin
|
||
| 825 | exec_macro(macro, obj, args) |
||
| 826 | rescue => e
|
||
| 827 | "<div class=\"flash error\">Error executing the <strong>#{macro}</strong> macro (#{e})</div>"
|
||
| 828 | end || all
|
||
| 829 | else
|
||
| 830 | all |
||
| 831 | end
|
||
| 832 | end
|
||
| 833 | end
|
||
| 834 | |||
| 835 | 119:8661b858af72 | Chris | TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE) |
| 836 | 441:cbce1fd3b1b7 | Chris | |
| 837 | 119:8661b858af72 | Chris | # Renders the TOC with given headings
|
| 838 | def replace_toc(text, headings) |
||
| 839 | 37:94944d00e43c | chris | text.gsub!(TOC_RE) do |
| 840 | if headings.empty?
|
||
| 841 | ''
|
||
| 842 | else
|
||
| 843 | div_class = 'toc'
|
||
| 844 | div_class << ' right' if $1 == '>' |
||
| 845 | div_class << ' left' if $1 == '<' |
||
| 846 | out = "<ul class=\"#{div_class}\"><li>"
|
||
| 847 | root = headings.map(&:first).min
|
||
| 848 | current = root |
||
| 849 | started = false
|
||
| 850 | headings.each do |level, anchor, item|
|
||
| 851 | if level > current
|
||
| 852 | out << '<ul><li>' * (level - current)
|
||
| 853 | elsif level < current
|
||
| 854 | out << "</li></ul>\n" * (current - level) + "</li><li>" |
||
| 855 | elsif started
|
||
| 856 | out << '</li><li>'
|
||
| 857 | end
|
||
| 858 | out << "<a href=\"##{anchor}\">#{item}</a>"
|
||
| 859 | current = level |
||
| 860 | started = true
|
||
| 861 | end
|
||
| 862 | out << '</li></ul>' * (current - root)
|
||
| 863 | out << '</li></ul>'
|
||
| 864 | end
|
||
| 865 | end
|
||
| 866 | end
|
||
| 867 | 0:513646585e45 | Chris | |
| 868 | # Same as Rails' simple_format helper without using paragraphs
|
||
| 869 | def simple_format_without_paragraph(text) |
||
| 870 | text.to_s. |
||
| 871 | gsub(/\r\n?/, "\n"). # \r\n and \r -> \n |
||
| 872 | gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br |
||
| 873 | 909:cbb26bc654de | Chris | gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br |
| 874 | html_safe |
||
| 875 | 0:513646585e45 | Chris | end
|
| 876 | |||
| 877 | def lang_options_for_select(blank=true) |
||
| 878 | (blank ? [["(auto)", ""]] : []) + |
||
| 879 | valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
|
||
| 880 | end
|
||
| 881 | |||
| 882 | def label_tag_for(name, option_tags = nil, options = {}) |
||
| 883 | label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "") |
||
| 884 | content_tag("label", label_text)
|
||
| 885 | end
|
||
| 886 | |||
| 887 | 909:cbb26bc654de | Chris | def labelled_tabular_form_for(*args, &proc) |
| 888 | args << {} unless args.last.is_a?(Hash)
|
||
| 889 | options = args.last |
||
| 890 | 0:513646585e45 | Chris | options[:html] ||= {}
|
| 891 | options[:html][:class] = 'tabular' unless options[:html].has_key?(:class) |
||
| 892 | 909:cbb26bc654de | Chris | options.merge!({:builder => TabularFormBuilder})
|
| 893 | form_for(*args, &proc) |
||
| 894 | end
|
||
| 895 | |||
| 896 | def labelled_form_for(*args, &proc) |
||
| 897 | args << {} unless args.last.is_a?(Hash)
|
||
| 898 | options = args.last |
||
| 899 | options.merge!({:builder => TabularFormBuilder})
|
||
| 900 | form_for(*args, &proc) |
||
| 901 | 0:513646585e45 | Chris | end
|
| 902 | |||
| 903 | def back_url_hidden_field_tag |
||
| 904 | back_url = params[:back_url] || request.env['HTTP_REFERER'] |
||
| 905 | back_url = CGI.unescape(back_url.to_s)
|
||
| 906 | hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank? |
||
| 907 | end
|
||
| 908 | |||
| 909 | def check_all_links(form_name) |
||
| 910 | link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") + |
||
| 911 | 909:cbb26bc654de | Chris | " | ".html_safe +
|
| 912 | 0:513646585e45 | Chris | link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)") |
| 913 | end
|
||
| 914 | |||
| 915 | def progress_bar(pcts, options={}) |
||
| 916 | pcts = [pcts, pcts] unless pcts.is_a?(Array) |
||
| 917 | pcts = pcts.collect(&:round)
|
||
| 918 | pcts[1] = pcts[1] - pcts[0] |
||
| 919 | pcts << (100 - pcts[1] - pcts[0]) |
||
| 920 | width = options[:width] || '100px;' |
||
| 921 | legend = options[:legend] || '' |
||
| 922 | content_tag('table',
|
||
| 923 | content_tag('tr',
|
||
| 924 | 909:cbb26bc654de | Chris | (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) + |
| 925 | (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) + |
||
| 926 | (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe) |
||
| 927 | ), :class => 'progress', :style => "width: #{width};").html_safe + |
||
| 928 | content_tag('p', legend, :class => 'pourcent').html_safe |
||
| 929 | 0:513646585e45 | Chris | end
|
| 930 | 441:cbce1fd3b1b7 | Chris | |
| 931 | 0:513646585e45 | Chris | def checked_image(checked=true) |
| 932 | if checked
|
||
| 933 | image_tag 'toggle_check.png'
|
||
| 934 | end
|
||
| 935 | end
|
||
| 936 | 441:cbce1fd3b1b7 | Chris | |
| 937 | 0:513646585e45 | Chris | def context_menu(url) |
| 938 | unless @context_menu_included |
||
| 939 | content_for :header_tags do |
||
| 940 | javascript_include_tag('context_menu') +
|
||
| 941 | stylesheet_link_tag('context_menu')
|
||
| 942 | end
|
||
| 943 | 14:1d32c0a0efbf | Chris | if l(:direction) == 'rtl' |
| 944 | content_for :header_tags do |
||
| 945 | stylesheet_link_tag('context_menu_rtl')
|
||
| 946 | end
|
||
| 947 | end
|
||
| 948 | 0:513646585e45 | Chris | @context_menu_included = true |
| 949 | end
|
||
| 950 | javascript_tag "new ContextMenu('#{ url_for(url) }')"
|
||
| 951 | end
|
||
| 952 | |||
| 953 | def context_menu_link(name, url, options={}) |
||
| 954 | options[:class] ||= '' |
||
| 955 | if options.delete(:selected) |
||
| 956 | options[:class] << ' icon-checked disabled' |
||
| 957 | options[:disabled] = true |
||
| 958 | end
|
||
| 959 | if options.delete(:disabled) |
||
| 960 | options.delete(:method)
|
||
| 961 | options.delete(:confirm)
|
||
| 962 | options.delete(:onclick)
|
||
| 963 | options[:class] << ' disabled' |
||
| 964 | url = '#'
|
||
| 965 | end
|
||
| 966 | 909:cbb26bc654de | Chris | link_to h(name), url, options |
| 967 | 0:513646585e45 | Chris | end
|
| 968 | |||
| 969 | def calendar_for(field_id) |
||
| 970 | include_calendar_headers_tags |
||
| 971 | image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) + |
||
| 972 | javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
|
||
| 973 | end
|
||
| 974 | |||
| 975 | def include_calendar_headers_tags |
||
| 976 | unless @calendar_headers_tags_included |
||
| 977 | @calendar_headers_tags_included = true |
||
| 978 | content_for :header_tags do |
||
| 979 | start_of_week = case Setting.start_of_week.to_i |
||
| 980 | when 1 |
||
| 981 | 'Calendar._FD = 1;' # Monday |
||
| 982 | when 7 |
||
| 983 | 'Calendar._FD = 0;' # Sunday |
||
| 984 | 441:cbce1fd3b1b7 | Chris | when 6 |
| 985 | 'Calendar._FD = 6;' # Saturday |
||
| 986 | 0:513646585e45 | Chris | else
|
| 987 | '' # use language |
||
| 988 | end
|
||
| 989 | 441:cbce1fd3b1b7 | Chris | |
| 990 | 0:513646585e45 | Chris | javascript_include_tag('calendar/calendar') +
|
| 991 | javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
|
||
| 992 | 441:cbce1fd3b1b7 | Chris | javascript_tag(start_of_week) + |
| 993 | 0:513646585e45 | Chris | javascript_include_tag('calendar/calendar-setup') +
|
| 994 | stylesheet_link_tag('calendar')
|
||
| 995 | end
|
||
| 996 | end
|
||
| 997 | end
|
||
| 998 | |||
| 999 | def content_for(name, content = nil, &block) |
||
| 1000 | @has_content ||= {}
|
||
| 1001 | @has_content[name] = true |
||
| 1002 | super(name, content, &block)
|
||
| 1003 | end
|
||
| 1004 | |||
| 1005 | def has_content?(name) |
||
| 1006 | (@has_content && @has_content[name]) || false |
||
| 1007 | end
|
||
| 1008 | |||
| 1009 | 909:cbb26bc654de | Chris | def email_delivery_enabled? |
| 1010 | !!ActionMailer::Base.perform_deliveries |
||
| 1011 | end
|
||
| 1012 | |||
| 1013 | 0:513646585e45 | Chris | # Returns the avatar image tag for the given +user+ if avatars are enabled
|
| 1014 | # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
|
||
| 1015 | def avatar(user, options = { }) |
||
| 1016 | if Setting.gravatar_enabled? |
||
| 1017 | 22:40f7cfd4df19 | chris | options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
|
| 1018 | 0:513646585e45 | Chris | email = nil
|
| 1019 | if user.respond_to?(:mail) |
||
| 1020 | email = user.mail |
||
| 1021 | elsif user.to_s =~ %r{<(.+?)>} |
||
| 1022 | email = $1
|
||
| 1023 | end
|
||
| 1024 | return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil |
||
| 1025 | 22:40f7cfd4df19 | chris | else
|
| 1026 | ''
|
||
| 1027 | 0:513646585e45 | Chris | end
|
| 1028 | end
|
||
| 1029 | 441:cbce1fd3b1b7 | Chris | |
| 1030 | 909:cbb26bc654de | Chris | def sanitize_anchor_name(anchor) |
| 1031 | anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-') |
||
| 1032 | end
|
||
| 1033 | |||
| 1034 | 245:051f544170fe | Chris | # Returns the javascript tags that are included in the html layout head
|
| 1035 | def javascript_heads |
||
| 1036 | tags = javascript_include_tag(:defaults)
|
||
| 1037 | unless User.current.pref.warn_on_leaving_unsaved == '0' |
||
| 1038 | 909:cbb26bc654de | Chris | tags << "\n".html_safe + javascript_tag("Event.observe(window, 'load', function(){ new WarnLeavingUnsaved('#{escape_javascript( l(:text_warn_on_leaving_unsaved) )}'); });") |
| 1039 | 245:051f544170fe | Chris | end
|
| 1040 | tags |
||
| 1041 | end
|
||
| 1042 | 0:513646585e45 | Chris | |
| 1043 | 14:1d32c0a0efbf | Chris | def favicon |
| 1044 | 909:cbb26bc654de | Chris | "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
|
| 1045 | 14:1d32c0a0efbf | Chris | end
|
| 1046 | 441:cbce1fd3b1b7 | Chris | |
| 1047 | def robot_exclusion_tag |
||
| 1048 | 909:cbb26bc654de | Chris | '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
|
| 1049 | 441:cbce1fd3b1b7 | Chris | end
|
| 1050 | |||
| 1051 | 503:5d608412b003 | chris | def stylesheet_platform_font_tag |
| 1052 | agent = request.env['HTTP_USER_AGENT']
|
||
| 1053 | name = 'fonts-generic'
|
||
| 1054 | if agent and agent =~ %r{Windows} |
||
| 1055 | name = 'fonts-ms'
|
||
| 1056 | elsif agent and agent =~ %r{Macintosh} |
||
| 1057 | name = 'fonts-mac'
|
||
| 1058 | end
|
||
| 1059 | stylesheet_link_tag name, :media => 'all' |
||
| 1060 | end
|
||
| 1061 | |||
| 1062 | 119:8661b858af72 | Chris | # Returns true if arg is expected in the API response
|
| 1063 | def include_in_api_response?(arg) |
||
| 1064 | unless @included_in_api_response |
||
| 1065 | param = params[:include]
|
||
| 1066 | @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',') |
||
| 1067 | @included_in_api_response.collect!(&:strip) |
||
| 1068 | end
|
||
| 1069 | @included_in_api_response.include?(arg.to_s)
|
||
| 1070 | end
|
||
| 1071 | 14:1d32c0a0efbf | Chris | |
| 1072 | 119:8661b858af72 | Chris | # Returns options or nil if nometa param or X-Redmine-Nometa header
|
| 1073 | # was set in the request
|
||
| 1074 | def api_meta(options) |
||
| 1075 | if params[:nometa].present? || request.headers['X-Redmine-Nometa'] |
||
| 1076 | # compatibility mode for activeresource clients that raise
|
||
| 1077 | # an error when unserializing an array with attributes
|
||
| 1078 | nil
|
||
| 1079 | else
|
||
| 1080 | options |
||
| 1081 | end
|
||
| 1082 | end
|
||
| 1083 | 441:cbce1fd3b1b7 | Chris | |
| 1084 | 0:513646585e45 | Chris | private |
| 1085 | |||
| 1086 | def wiki_helper |
||
| 1087 | helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting) |
||
| 1088 | extend helper |
||
| 1089 | return self |
||
| 1090 | end
|
||
| 1091 | 441:cbce1fd3b1b7 | Chris | |
| 1092 | def link_to_content_update(text, url_params = {}, html_options = {}) |
||
| 1093 | link_to(text, url_params, html_options) |
||
| 1094 | 0:513646585e45 | Chris | end
|
| 1095 | end |