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