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