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