##// END OF EJS Templates
fix typos of source comments at ApplicationHelper...
Toshi MARUYAMA -
r12796:92509940881b
parent child
Show More
@@ -1,1352 +1,1352
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 case prefix
826 826 when 'document'
827 827 if project && document = project.documents.visible.find_by_title(name)
828 828 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
829 829 :class => 'document'
830 830 end
831 831 when 'version'
832 832 if project && version = project.versions.visible.find_by_name(name)
833 833 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
834 834 :class => 'version'
835 835 end
836 836 when 'forum'
837 837 if project && board = project.boards.visible.find_by_name(name)
838 838 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
839 839 :class => 'board'
840 840 end
841 841 when 'news'
842 842 if project && news = project.news.visible.find_by_title(name)
843 843 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
844 844 :class => 'news'
845 845 end
846 846 when 'commit', 'source', 'export'
847 847 if project
848 848 repository = nil
849 849 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
850 850 repo_prefix, repo_identifier, name = $1, $2, $3
851 851 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
852 852 else
853 853 repository = project.repository
854 854 end
855 855 if prefix == 'commit'
856 856 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
857 857 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},
858 858 :class => 'changeset',
859 859 :title => truncate_single_line_raw(changeset.comments, 100)
860 860 end
861 861 else
862 862 if repository && User.current.allowed_to?(:browse_repository, project)
863 863 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
864 864 path, rev, anchor = $1, $3, $5
865 865 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 866 :path => to_path_param(path),
867 867 :rev => rev,
868 868 :anchor => anchor},
869 869 :class => (prefix == 'export' ? 'source download' : 'source')
870 870 end
871 871 end
872 872 repo_prefix = nil
873 873 end
874 874 when 'attachment'
875 875 attachments = options[:attachments] || []
876 876 attachments += obj.attachments if obj.respond_to?(:attachments)
877 877 if attachments && attachment = Attachment.latest_attach(attachments, name)
878 878 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
879 879 end
880 880 when 'project'
881 881 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
882 882 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
883 883 end
884 884 end
885 885 end
886 886 end
887 887 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
888 888 end
889 889 end
890 890
891 891 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
892 892
893 893 def parse_sections(text, project, obj, attr, only_path, options)
894 894 return unless options[:edit_section_links]
895 895 text.gsub!(HEADING_RE) do
896 896 heading = $1
897 897 @current_section += 1
898 898 if @current_section > 1
899 899 content_tag('div',
900 900 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
901 901 :class => 'contextual',
902 902 :title => l(:button_edit_section),
903 903 :id => "section-#{@current_section}") + heading.html_safe
904 904 else
905 905 heading
906 906 end
907 907 end
908 908 end
909 909
910 910 # Headings and TOC
911 911 # Adds ids and links to headings unless options[:headings] is set to false
912 912 def parse_headings(text, project, obj, attr, only_path, options)
913 913 return if options[:headings] == false
914 914
915 915 text.gsub!(HEADING_RE) do
916 916 level, attrs, content = $2.to_i, $3, $4
917 917 item = strip_tags(content).strip
918 918 anchor = sanitize_anchor_name(item)
919 919 # used for single-file wiki export
920 920 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
921 921 @heading_anchors[anchor] ||= 0
922 922 idx = (@heading_anchors[anchor] += 1)
923 923 if idx > 1
924 924 anchor = "#{anchor}-#{idx}"
925 925 end
926 926 @parsed_headings << [level, anchor, item]
927 927 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
928 928 end
929 929 end
930 930
931 931 MACROS_RE = /(
932 932 (!)? # escaping
933 933 (
934 934 \{\{ # opening tag
935 935 ([\w]+) # macro name
936 936 (\(([^\n\r]*?)\))? # optional arguments
937 937 ([\n\r].*?[\n\r])? # optional block of text
938 938 \}\} # closing tag
939 939 )
940 940 )/mx unless const_defined?(:MACROS_RE)
941 941
942 942 MACRO_SUB_RE = /(
943 943 \{\{
944 944 macro\((\d+)\)
945 945 \}\}
946 946 )/x unless const_defined?(:MACRO_SUB_RE)
947 947
948 948 # Extracts macros from text
949 949 def catch_macros(text)
950 950 macros = {}
951 951 text.gsub!(MACROS_RE) do
952 952 all, macro = $1, $4.downcase
953 953 if macro_exists?(macro) || all =~ MACRO_SUB_RE
954 954 index = macros.size
955 955 macros[index] = all
956 956 "{{macro(#{index})}}"
957 957 else
958 958 all
959 959 end
960 960 end
961 961 macros
962 962 end
963 963
964 964 # Executes and replaces macros in text
965 965 def inject_macros(text, obj, macros, execute=true)
966 966 text.gsub!(MACRO_SUB_RE) do
967 967 all, index = $1, $2.to_i
968 968 orig = macros.delete(index)
969 969 if execute && orig && orig =~ MACROS_RE
970 970 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
971 971 if esc.nil?
972 972 h(exec_macro(macro, obj, args, block) || all)
973 973 else
974 974 h(all)
975 975 end
976 976 elsif orig
977 977 h(orig)
978 978 else
979 979 h(all)
980 980 end
981 981 end
982 982 end
983 983
984 984 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
985 985
986 986 # Renders the TOC with given headings
987 987 def replace_toc(text, headings)
988 988 text.gsub!(TOC_RE) do
989 989 left_align, right_align = $2, $3
990 990 # Keep only the 4 first levels
991 991 headings = headings.select{|level, anchor, item| level <= 4}
992 992 if headings.empty?
993 993 ''
994 994 else
995 995 div_class = 'toc'
996 996 div_class << ' right' if right_align
997 997 div_class << ' left' if left_align
998 998 out = "<ul class=\"#{div_class}\"><li>"
999 999 root = headings.map(&:first).min
1000 1000 current = root
1001 1001 started = false
1002 1002 headings.each do |level, anchor, item|
1003 1003 if level > current
1004 1004 out << '<ul><li>' * (level - current)
1005 1005 elsif level < current
1006 1006 out << "</li></ul>\n" * (current - level) + "</li><li>"
1007 1007 elsif started
1008 1008 out << '</li><li>'
1009 1009 end
1010 1010 out << "<a href=\"##{anchor}\">#{item}</a>"
1011 1011 current = level
1012 1012 started = true
1013 1013 end
1014 1014 out << '</li></ul>' * (current - root)
1015 1015 out << '</li></ul>'
1016 1016 end
1017 1017 end
1018 1018 end
1019 1019
1020 1020 # Same as Rails' simple_format helper without using paragraphs
1021 1021 def simple_format_without_paragraph(text)
1022 1022 text.to_s.
1023 1023 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1024 1024 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1025 1025 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1026 1026 html_safe
1027 1027 end
1028 1028
1029 1029 def lang_options_for_select(blank=true)
1030 1030 (blank ? [["(auto)", ""]] : []) + languages_options
1031 1031 end
1032 1032
1033 1033 def label_tag_for(name, option_tags = nil, options = {})
1034 1034 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
1035 1035 content_tag("label", label_text)
1036 1036 end
1037 1037
1038 1038 def labelled_form_for(*args, &proc)
1039 1039 args << {} unless args.last.is_a?(Hash)
1040 1040 options = args.last
1041 1041 if args.first.is_a?(Symbol)
1042 1042 options.merge!(:as => args.shift)
1043 1043 end
1044 1044 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1045 1045 form_for(*args, &proc)
1046 1046 end
1047 1047
1048 1048 def labelled_fields_for(*args, &proc)
1049 1049 args << {} unless args.last.is_a?(Hash)
1050 1050 options = args.last
1051 1051 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1052 1052 fields_for(*args, &proc)
1053 1053 end
1054 1054
1055 1055 def labelled_remote_form_for(*args, &proc)
1056 1056 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_remote_form_for is deprecated and will be removed in Redmine 2.2."
1057 1057 args << {} unless args.last.is_a?(Hash)
1058 1058 options = args.last
1059 1059 options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
1060 1060 form_for(*args, &proc)
1061 1061 end
1062 1062
1063 1063 def error_messages_for(*objects)
1064 1064 html = ""
1065 1065 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1066 1066 errors = objects.map {|o| o.errors.full_messages}.flatten
1067 1067 if errors.any?
1068 1068 html << "<div id='errorExplanation'><ul>\n"
1069 1069 errors.each do |error|
1070 1070 html << "<li>#{h error}</li>\n"
1071 1071 end
1072 1072 html << "</ul></div>\n"
1073 1073 end
1074 1074 html.html_safe
1075 1075 end
1076 1076
1077 1077 def delete_link(url, options={})
1078 1078 options = {
1079 1079 :method => :delete,
1080 1080 :data => {:confirm => l(:text_are_you_sure)},
1081 1081 :class => 'icon icon-del'
1082 1082 }.merge(options)
1083 1083
1084 1084 link_to l(:button_delete), url, options
1085 1085 end
1086 1086
1087 1087 def preview_link(url, form, target='preview', options={})
1088 1088 content_tag 'a', l(:label_preview), {
1089 1089 :href => "#",
1090 1090 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1091 1091 :accesskey => accesskey(:preview)
1092 1092 }.merge(options)
1093 1093 end
1094 1094
1095 1095 def link_to_function(name, function, html_options={})
1096 1096 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1097 1097 end
1098 1098
1099 1099 # Helper to render JSON in views
1100 1100 def raw_json(arg)
1101 1101 arg.to_json.to_s.gsub('/', '\/').html_safe
1102 1102 end
1103 1103
1104 1104 def back_url
1105 1105 url = params[:back_url]
1106 1106 if url.nil? && referer = request.env['HTTP_REFERER']
1107 1107 url = CGI.unescape(referer.to_s)
1108 1108 end
1109 1109 url
1110 1110 end
1111 1111
1112 1112 def back_url_hidden_field_tag
1113 1113 url = back_url
1114 1114 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1115 1115 end
1116 1116
1117 1117 def check_all_links(form_name)
1118 1118 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1119 1119 " | ".html_safe +
1120 1120 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1121 1121 end
1122 1122
1123 1123 def progress_bar(pcts, options={})
1124 1124 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1125 1125 pcts = pcts.collect(&:round)
1126 1126 pcts[1] = pcts[1] - pcts[0]
1127 1127 pcts << (100 - pcts[1] - pcts[0])
1128 1128 width = options[:width] || '100px;'
1129 1129 legend = options[:legend] || ''
1130 1130 content_tag('table',
1131 1131 content_tag('tr',
1132 1132 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1133 1133 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1134 1134 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1135 1135 ), :class => "progress progress-#{pcts[0]}", :style => "width: #{width};").html_safe +
1136 1136 content_tag('p', legend, :class => 'percent').html_safe
1137 1137 end
1138 1138
1139 1139 def checked_image(checked=true)
1140 1140 if checked
1141 1141 image_tag 'toggle_check.png'
1142 1142 end
1143 1143 end
1144 1144
1145 1145 def context_menu(url)
1146 1146 unless @context_menu_included
1147 1147 content_for :header_tags do
1148 1148 javascript_include_tag('context_menu') +
1149 1149 stylesheet_link_tag('context_menu')
1150 1150 end
1151 1151 if l(:direction) == 'rtl'
1152 1152 content_for :header_tags do
1153 1153 stylesheet_link_tag('context_menu_rtl')
1154 1154 end
1155 1155 end
1156 1156 @context_menu_included = true
1157 1157 end
1158 1158 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1159 1159 end
1160 1160
1161 1161 def calendar_for(field_id)
1162 1162 include_calendar_headers_tags
1163 1163 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1164 1164 end
1165 1165
1166 1166 def include_calendar_headers_tags
1167 1167 unless @calendar_headers_tags_included
1168 1168 tags = javascript_include_tag("datepicker")
1169 1169 @calendar_headers_tags_included = true
1170 1170 content_for :header_tags do
1171 1171 start_of_week = Setting.start_of_week
1172 1172 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1173 1173 # Redmine uses 1..7 (monday..sunday) in settings and locales
1174 1174 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1175 1175 start_of_week = start_of_week.to_i % 7
1176 1176 tags << javascript_tag(
1177 1177 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1178 1178 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1179 1179 path_to_image('/images/calendar.png') +
1180 1180 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1181 1181 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1182 1182 "beforeShow: beforeShowDatePicker};")
1183 1183 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1184 1184 unless jquery_locale == 'en'
1185 1185 tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js")
1186 1186 end
1187 1187 tags
1188 1188 end
1189 1189 end
1190 1190 end
1191 1191
1192 1192 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1193 1193 # Examples:
1194 1194 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1195 1195 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1196 1196 #
1197 1197 def stylesheet_link_tag(*sources)
1198 1198 options = sources.last.is_a?(Hash) ? sources.pop : {}
1199 1199 plugin = options.delete(:plugin)
1200 1200 sources = sources.map do |source|
1201 1201 if plugin
1202 1202 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1203 1203 elsif current_theme && current_theme.stylesheets.include?(source)
1204 1204 current_theme.stylesheet_path(source)
1205 1205 else
1206 1206 source
1207 1207 end
1208 1208 end
1209 1209 super sources, options
1210 1210 end
1211 1211
1212 1212 # Overrides Rails' image_tag with themes and plugins support.
1213 1213 # Examples:
1214 1214 # image_tag('image.png') # => picks image.png from the current theme or defaults
1215 1215 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1216 1216 #
1217 1217 def image_tag(source, options={})
1218 1218 if plugin = options.delete(:plugin)
1219 1219 source = "/plugin_assets/#{plugin}/images/#{source}"
1220 1220 elsif current_theme && current_theme.images.include?(source)
1221 1221 source = current_theme.image_path(source)
1222 1222 end
1223 1223 super source, options
1224 1224 end
1225 1225
1226 1226 # Overrides Rails' javascript_include_tag with plugins support
1227 1227 # Examples:
1228 1228 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1229 1229 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1230 1230 #
1231 1231 def javascript_include_tag(*sources)
1232 1232 options = sources.last.is_a?(Hash) ? sources.pop : {}
1233 1233 if plugin = options.delete(:plugin)
1234 1234 sources = sources.map do |source|
1235 1235 if plugin
1236 1236 "/plugin_assets/#{plugin}/javascripts/#{source}"
1237 1237 else
1238 1238 source
1239 1239 end
1240 1240 end
1241 1241 end
1242 1242 super sources, options
1243 1243 end
1244 1244
1245 1245 # TODO: remove this in 2.5.0
1246 1246 def has_content?(name)
1247 1247 content_for?(name)
1248 1248 end
1249 1249
1250 1250 def sidebar_content?
1251 1251 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1252 1252 end
1253 1253
1254 1254 def view_layouts_base_sidebar_hook_response
1255 1255 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1256 1256 end
1257 1257
1258 1258 def email_delivery_enabled?
1259 1259 !!ActionMailer::Base.perform_deliveries
1260 1260 end
1261 1261
1262 1262 # Returns the avatar image tag for the given +user+ if avatars are enabled
1263 1263 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1264 1264 def avatar(user, options = { })
1265 1265 if Setting.gravatar_enabled?
1266 1266 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1267 1267 email = nil
1268 1268 if user.respond_to?(:mail)
1269 1269 email = user.mail
1270 1270 elsif user.to_s =~ %r{<(.+?)>}
1271 1271 email = $1
1272 1272 end
1273 1273 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1274 1274 else
1275 1275 ''
1276 1276 end
1277 1277 end
1278 1278
1279 1279 def sanitize_anchor_name(anchor)
1280 1280 if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java'
1281 1281 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1282 1282 else
1283 1283 # TODO: remove when ruby1.8 is no longer supported
1284 1284 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1285 1285 end
1286 1286 end
1287 1287
1288 1288 # Returns the javascript tags that are included in the html layout head
1289 1289 def javascript_heads
1290 1290 tags = javascript_include_tag('jquery-1.8.3-ui-1.9.2-ujs-2.0.3', 'application')
1291 1291 unless User.current.pref.warn_on_leaving_unsaved == '0'
1292 1292 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1293 1293 end
1294 1294 tags
1295 1295 end
1296 1296
1297 1297 def favicon
1298 1298 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1299 1299 end
1300 1300
1301 1301 # Returns the path to the favicon
1302 1302 def favicon_path
1303 1303 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1304 1304 image_path(icon)
1305 1305 end
1306 1306
1307 1307 # Returns the full URL to the favicon
1308 1308 def favicon_url
1309 1309 # TODO: use #image_url introduced in Rails4
1310 1310 path = favicon_path
1311 1311 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1312 1312 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1313 1313 end
1314 1314
1315 1315 def robot_exclusion_tag
1316 1316 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1317 1317 end
1318 1318
1319 1319 # Returns true if arg is expected in the API response
1320 1320 def include_in_api_response?(arg)
1321 1321 unless @included_in_api_response
1322 1322 param = params[:include]
1323 1323 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1324 1324 @included_in_api_response.collect!(&:strip)
1325 1325 end
1326 1326 @included_in_api_response.include?(arg.to_s)
1327 1327 end
1328 1328
1329 1329 # Returns options or nil if nometa param or X-Redmine-Nometa header
1330 1330 # was set in the request
1331 1331 def api_meta(options)
1332 1332 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1333 1333 # compatibility mode for activeresource clients that raise
1334 # an error when unserializing an array with attributes
1334 # an error when deserializing an array with attributes
1335 1335 nil
1336 1336 else
1337 1337 options
1338 1338 end
1339 1339 end
1340 1340
1341 1341 private
1342 1342
1343 1343 def wiki_helper
1344 1344 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1345 1345 extend helper
1346 1346 return self
1347 1347 end
1348 1348
1349 1349 def link_to_content_update(text, url_params = {}, html_options = {})
1350 1350 link_to(text, url_params, html_options)
1351 1351 end
1352 1352 end
General Comments 0
You need to be logged in to leave comments. Login now