##// END OF EJS Templates
Document project links in ApplicationHelper#parse_redmine_links (#6689)....
Jean-Baptiste Barth -
r11573:1e7c26d9a5ed
parent child
Show More
@@ -1,1248 +1,1251
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) || element.id.to_s == 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 def option_tag(name, text, value, selected=nil, options={})
352 352 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
353 353 end
354 354
355 355 # Truncates and returns the string as a single line
356 356 def truncate_single_line(string, *args)
357 357 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
358 358 end
359 359
360 360 # Truncates at line break after 250 characters or options[:length]
361 361 def truncate_lines(string, options={})
362 362 length = options[:length] || 250
363 363 if string.to_s =~ /\A(.{#{length}}.*?)$/m
364 364 "#{$1}..."
365 365 else
366 366 string
367 367 end
368 368 end
369 369
370 370 def anchor(text)
371 371 text.to_s.gsub(' ', '_')
372 372 end
373 373
374 374 def html_hours(text)
375 375 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
376 376 end
377 377
378 378 def authoring(created, author, options={})
379 379 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
380 380 end
381 381
382 382 def time_tag(time)
383 383 text = distance_of_time_in_words(Time.now, time)
384 384 if @project
385 385 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
386 386 else
387 387 content_tag('acronym', text, :title => format_time(time))
388 388 end
389 389 end
390 390
391 391 def syntax_highlight_lines(name, content)
392 392 lines = []
393 393 syntax_highlight(name, content).each_line { |line| lines << line }
394 394 lines
395 395 end
396 396
397 397 def syntax_highlight(name, content)
398 398 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
399 399 end
400 400
401 401 def to_path_param(path)
402 402 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
403 403 str.blank? ? nil : str
404 404 end
405 405
406 406 def reorder_links(name, url, method = :post)
407 407 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
408 408 url.merge({"#{name}[move_to]" => 'highest'}),
409 409 :method => method, :title => l(:label_sort_highest)) +
410 410 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
411 411 url.merge({"#{name}[move_to]" => 'higher'}),
412 412 :method => method, :title => l(:label_sort_higher)) +
413 413 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
414 414 url.merge({"#{name}[move_to]" => 'lower'}),
415 415 :method => method, :title => l(:label_sort_lower)) +
416 416 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
417 417 url.merge({"#{name}[move_to]" => 'lowest'}),
418 418 :method => method, :title => l(:label_sort_lowest))
419 419 end
420 420
421 421 def breadcrumb(*args)
422 422 elements = args.flatten
423 423 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
424 424 end
425 425
426 426 def other_formats_links(&block)
427 427 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
428 428 yield Redmine::Views::OtherFormatsBuilder.new(self)
429 429 concat('</p>'.html_safe)
430 430 end
431 431
432 432 def page_header_title
433 433 if @project.nil? || @project.new_record?
434 434 h(Setting.app_title)
435 435 else
436 436 b = []
437 437 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
438 438 if ancestors.any?
439 439 root = ancestors.shift
440 440 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
441 441 if ancestors.size > 2
442 442 b << "\xe2\x80\xa6"
443 443 ancestors = ancestors[-2, 2]
444 444 end
445 445 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
446 446 end
447 447 b << h(@project)
448 448 b.join(" \xc2\xbb ").html_safe
449 449 end
450 450 end
451 451
452 452 def html_title(*args)
453 453 if args.empty?
454 454 title = @html_title || []
455 455 title << @project.name if @project
456 456 title << Setting.app_title unless Setting.app_title == title.last
457 457 title.select {|t| !t.blank? }.join(' - ')
458 458 else
459 459 @html_title ||= []
460 460 @html_title += args
461 461 end
462 462 end
463 463
464 464 # Returns the theme, controller name, and action as css classes for the
465 465 # HTML body.
466 466 def body_css_classes
467 467 css = []
468 468 if theme = Redmine::Themes.theme(Setting.ui_theme)
469 469 css << 'theme-' + theme.name
470 470 end
471 471
472 472 css << 'controller-' + controller_name
473 473 css << 'action-' + action_name
474 474 css.join(' ')
475 475 end
476 476
477 477 def accesskey(s)
478 478 @used_accesskeys ||= []
479 479 key = Redmine::AccessKeys.key_for(s)
480 480 return nil if @used_accesskeys.include?(key)
481 481 @used_accesskeys << key
482 482 key
483 483 end
484 484
485 485 # Formats text according to system settings.
486 486 # 2 ways to call this method:
487 487 # * with a String: textilizable(text, options)
488 488 # * with an object and one of its attribute: textilizable(issue, :description, options)
489 489 def textilizable(*args)
490 490 options = args.last.is_a?(Hash) ? args.pop : {}
491 491 case args.size
492 492 when 1
493 493 obj = options[:object]
494 494 text = args.shift
495 495 when 2
496 496 obj = args.shift
497 497 attr = args.shift
498 498 text = obj.send(attr).to_s
499 499 else
500 500 raise ArgumentError, 'invalid arguments to textilizable'
501 501 end
502 502 return '' if text.blank?
503 503 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
504 504 only_path = options.delete(:only_path) == false ? false : true
505 505
506 506 text = text.dup
507 507 macros = catch_macros(text)
508 508 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
509 509
510 510 @parsed_headings = []
511 511 @heading_anchors = {}
512 512 @current_section = 0 if options[:edit_section_links]
513 513
514 514 parse_sections(text, project, obj, attr, only_path, options)
515 515 text = parse_non_pre_blocks(text, obj, macros) do |text|
516 516 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
517 517 send method_name, text, project, obj, attr, only_path, options
518 518 end
519 519 end
520 520 parse_headings(text, project, obj, attr, only_path, options)
521 521
522 522 if @parsed_headings.any?
523 523 replace_toc(text, @parsed_headings)
524 524 end
525 525
526 526 text.html_safe
527 527 end
528 528
529 529 def parse_non_pre_blocks(text, obj, macros)
530 530 s = StringScanner.new(text)
531 531 tags = []
532 532 parsed = ''
533 533 while !s.eos?
534 534 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
535 535 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
536 536 if tags.empty?
537 537 yield text
538 538 inject_macros(text, obj, macros) if macros.any?
539 539 else
540 540 inject_macros(text, obj, macros, false) if macros.any?
541 541 end
542 542 parsed << text
543 543 if tag
544 544 if closing
545 545 if tags.last == tag.downcase
546 546 tags.pop
547 547 end
548 548 else
549 549 tags << tag.downcase
550 550 end
551 551 parsed << full_tag
552 552 end
553 553 end
554 554 # Close any non closing tags
555 555 while tag = tags.pop
556 556 parsed << "</#{tag}>"
557 557 end
558 558 parsed
559 559 end
560 560
561 561 def parse_inline_attachments(text, project, obj, attr, only_path, options)
562 562 # when using an image link, try to use an attachment, if possible
563 563 attachments = options[:attachments] || []
564 564 attachments += obj.attachments if obj.respond_to?(:attachments)
565 565 if attachments.present?
566 566 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
567 567 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
568 568 # search for the picture in attachments
569 569 if found = Attachment.latest_attach(attachments, filename)
570 570 image_url = download_named_attachment_path(found, found.filename, :only_path => only_path)
571 571 desc = found.description.to_s.gsub('"', '')
572 572 if !desc.blank? && alttext.blank?
573 573 alt = " title=\"#{desc}\" alt=\"#{desc}\""
574 574 end
575 575 "src=\"#{image_url}\"#{alt}"
576 576 else
577 577 m
578 578 end
579 579 end
580 580 end
581 581 end
582 582
583 583 # Wiki links
584 584 #
585 585 # Examples:
586 586 # [[mypage]]
587 587 # [[mypage|mytext]]
588 588 # wiki links can refer other project wikis, using project name or identifier:
589 589 # [[project:]] -> wiki starting page
590 590 # [[project:|mytext]]
591 591 # [[project:mypage]]
592 592 # [[project:mypage|mytext]]
593 593 def parse_wiki_links(text, project, obj, attr, only_path, options)
594 594 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
595 595 link_project = project
596 596 esc, all, page, title = $1, $2, $3, $5
597 597 if esc.nil?
598 598 if page =~ /^([^\:]+)\:(.*)$/
599 599 identifier, page = $1, $2
600 600 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
601 601 title ||= identifier if page.blank?
602 602 end
603 603
604 604 if link_project && link_project.wiki
605 605 # extract anchor
606 606 anchor = nil
607 607 if page =~ /^(.+?)\#(.+)$/
608 608 page, anchor = $1, $2
609 609 end
610 610 anchor = sanitize_anchor_name(anchor) if anchor.present?
611 611 # check if page exists
612 612 wiki_page = link_project.wiki.find_page(page)
613 613 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
614 614 "##{anchor}"
615 615 else
616 616 case options[:wiki_links]
617 617 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
618 618 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
619 619 else
620 620 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
621 621 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
622 622 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
623 623 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
624 624 end
625 625 end
626 626 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
627 627 else
628 628 # project or wiki doesn't exist
629 629 all
630 630 end
631 631 else
632 632 all
633 633 end
634 634 end
635 635 end
636 636
637 637 # Redmine links
638 638 #
639 639 # Examples:
640 640 # Issues:
641 641 # #52 -> Link to issue #52
642 642 # Changesets:
643 643 # r52 -> Link to revision 52
644 644 # commit:a85130f -> Link to scmid starting with a85130f
645 645 # Documents:
646 646 # document#17 -> Link to document with id 17
647 647 # document:Greetings -> Link to the document with title "Greetings"
648 648 # document:"Some document" -> Link to the document with title "Some document"
649 649 # Versions:
650 650 # version#3 -> Link to version with id 3
651 651 # version:1.0.0 -> Link to version named "1.0.0"
652 652 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
653 653 # Attachments:
654 654 # attachment:file.zip -> Link to the attachment of the current object named file.zip
655 655 # Source files:
656 656 # source:some/file -> Link to the file located at /some/file in the project's repository
657 657 # source:some/file@52 -> Link to the file's revision 52
658 658 # source:some/file#L120 -> Link to line 120 of the file
659 659 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
660 660 # export:some/file -> Force the download of the file
661 661 # Forum messages:
662 662 # message#1218 -> Link to message with id 1218
663 # Projects:
664 # project:someproject -> Link to project named "someproject"
665 # project#3 -> Link to project with id 3
663 666 #
664 667 # Links can refer other objects from other projects, using project identifier:
665 668 # identifier:r52
666 669 # identifier:document:"Some document"
667 670 # identifier:version:1.0.0
668 671 # identifier:source:some/file
669 672 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
670 673 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|
671 674 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
672 675 link = nil
673 676 project = default_project
674 677 if project_identifier
675 678 project = Project.visible.find_by_identifier(project_identifier)
676 679 end
677 680 if esc.nil?
678 681 if prefix.nil? && sep == 'r'
679 682 if project
680 683 repository = nil
681 684 if repo_identifier
682 685 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
683 686 else
684 687 repository = project.repository
685 688 end
686 689 # project.changesets.visible raises an SQL error because of a double join on repositories
687 690 if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier))
688 691 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},
689 692 :class => 'changeset',
690 693 :title => truncate_single_line(changeset.comments, :length => 100))
691 694 end
692 695 end
693 696 elsif sep == '#'
694 697 oid = identifier.to_i
695 698 case prefix
696 699 when nil
697 700 if oid.to_s == identifier && issue = Issue.visible.find_by_id(oid, :include => :status)
698 701 anchor = comment_id ? "note-#{comment_id}" : nil
699 702 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor},
700 703 :class => issue.css_classes,
701 704 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
702 705 end
703 706 when 'document'
704 707 if document = Document.visible.find_by_id(oid)
705 708 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
706 709 :class => 'document'
707 710 end
708 711 when 'version'
709 712 if version = Version.visible.find_by_id(oid)
710 713 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
711 714 :class => 'version'
712 715 end
713 716 when 'message'
714 717 if message = Message.visible.find_by_id(oid, :include => :parent)
715 718 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
716 719 end
717 720 when 'forum'
718 721 if board = Board.visible.find_by_id(oid)
719 722 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
720 723 :class => 'board'
721 724 end
722 725 when 'news'
723 726 if news = News.visible.find_by_id(oid)
724 727 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
725 728 :class => 'news'
726 729 end
727 730 when 'project'
728 731 if p = Project.visible.find_by_id(oid)
729 732 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
730 733 end
731 734 end
732 735 elsif sep == ':'
733 736 # removes the double quotes if any
734 737 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
735 738 case prefix
736 739 when 'document'
737 740 if project && document = project.documents.visible.find_by_title(name)
738 741 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
739 742 :class => 'document'
740 743 end
741 744 when 'version'
742 745 if project && version = project.versions.visible.find_by_name(name)
743 746 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
744 747 :class => 'version'
745 748 end
746 749 when 'forum'
747 750 if project && board = project.boards.visible.find_by_name(name)
748 751 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
749 752 :class => 'board'
750 753 end
751 754 when 'news'
752 755 if project && news = project.news.visible.find_by_title(name)
753 756 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
754 757 :class => 'news'
755 758 end
756 759 when 'commit', 'source', 'export'
757 760 if project
758 761 repository = nil
759 762 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
760 763 repo_prefix, repo_identifier, name = $1, $2, $3
761 764 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
762 765 else
763 766 repository = project.repository
764 767 end
765 768 if prefix == 'commit'
766 769 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
767 770 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},
768 771 :class => 'changeset',
769 772 :title => truncate_single_line(changeset.comments, :length => 100)
770 773 end
771 774 else
772 775 if repository && User.current.allowed_to?(:browse_repository, project)
773 776 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
774 777 path, rev, anchor = $1, $3, $5
775 778 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
776 779 :path => to_path_param(path),
777 780 :rev => rev,
778 781 :anchor => anchor},
779 782 :class => (prefix == 'export' ? 'source download' : 'source')
780 783 end
781 784 end
782 785 repo_prefix = nil
783 786 end
784 787 when 'attachment'
785 788 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
786 789 if attachments && attachment = Attachment.latest_attach(attachments, name)
787 790 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
788 791 end
789 792 when 'project'
790 793 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
791 794 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
792 795 end
793 796 end
794 797 end
795 798 end
796 799 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
797 800 end
798 801 end
799 802
800 803 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
801 804
802 805 def parse_sections(text, project, obj, attr, only_path, options)
803 806 return unless options[:edit_section_links]
804 807 text.gsub!(HEADING_RE) do
805 808 heading = $1
806 809 @current_section += 1
807 810 if @current_section > 1
808 811 content_tag('div',
809 812 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
810 813 :class => 'contextual',
811 814 :title => l(:button_edit_section)) + heading.html_safe
812 815 else
813 816 heading
814 817 end
815 818 end
816 819 end
817 820
818 821 # Headings and TOC
819 822 # Adds ids and links to headings unless options[:headings] is set to false
820 823 def parse_headings(text, project, obj, attr, only_path, options)
821 824 return if options[:headings] == false
822 825
823 826 text.gsub!(HEADING_RE) do
824 827 level, attrs, content = $2.to_i, $3, $4
825 828 item = strip_tags(content).strip
826 829 anchor = sanitize_anchor_name(item)
827 830 # used for single-file wiki export
828 831 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
829 832 @heading_anchors[anchor] ||= 0
830 833 idx = (@heading_anchors[anchor] += 1)
831 834 if idx > 1
832 835 anchor = "#{anchor}-#{idx}"
833 836 end
834 837 @parsed_headings << [level, anchor, item]
835 838 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
836 839 end
837 840 end
838 841
839 842 MACROS_RE = /(
840 843 (!)? # escaping
841 844 (
842 845 \{\{ # opening tag
843 846 ([\w]+) # macro name
844 847 (\(([^\n\r]*?)\))? # optional arguments
845 848 ([\n\r].*?[\n\r])? # optional block of text
846 849 \}\} # closing tag
847 850 )
848 851 )/mx unless const_defined?(:MACROS_RE)
849 852
850 853 MACRO_SUB_RE = /(
851 854 \{\{
852 855 macro\((\d+)\)
853 856 \}\}
854 857 )/x unless const_defined?(:MACRO_SUB_RE)
855 858
856 859 # Extracts macros from text
857 860 def catch_macros(text)
858 861 macros = {}
859 862 text.gsub!(MACROS_RE) do
860 863 all, macro = $1, $4.downcase
861 864 if macro_exists?(macro) || all =~ MACRO_SUB_RE
862 865 index = macros.size
863 866 macros[index] = all
864 867 "{{macro(#{index})}}"
865 868 else
866 869 all
867 870 end
868 871 end
869 872 macros
870 873 end
871 874
872 875 # Executes and replaces macros in text
873 876 def inject_macros(text, obj, macros, execute=true)
874 877 text.gsub!(MACRO_SUB_RE) do
875 878 all, index = $1, $2.to_i
876 879 orig = macros.delete(index)
877 880 if execute && orig && orig =~ MACROS_RE
878 881 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
879 882 if esc.nil?
880 883 h(exec_macro(macro, obj, args, block) || all)
881 884 else
882 885 h(all)
883 886 end
884 887 elsif orig
885 888 h(orig)
886 889 else
887 890 h(all)
888 891 end
889 892 end
890 893 end
891 894
892 895 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
893 896
894 897 # Renders the TOC with given headings
895 898 def replace_toc(text, headings)
896 899 text.gsub!(TOC_RE) do
897 900 # Keep only the 4 first levels
898 901 headings = headings.select{|level, anchor, item| level <= 4}
899 902 if headings.empty?
900 903 ''
901 904 else
902 905 div_class = 'toc'
903 906 div_class << ' right' if $1 == '>'
904 907 div_class << ' left' if $1 == '<'
905 908 out = "<ul class=\"#{div_class}\"><li>"
906 909 root = headings.map(&:first).min
907 910 current = root
908 911 started = false
909 912 headings.each do |level, anchor, item|
910 913 if level > current
911 914 out << '<ul><li>' * (level - current)
912 915 elsif level < current
913 916 out << "</li></ul>\n" * (current - level) + "</li><li>"
914 917 elsif started
915 918 out << '</li><li>'
916 919 end
917 920 out << "<a href=\"##{anchor}\">#{item}</a>"
918 921 current = level
919 922 started = true
920 923 end
921 924 out << '</li></ul>' * (current - root)
922 925 out << '</li></ul>'
923 926 end
924 927 end
925 928 end
926 929
927 930 # Same as Rails' simple_format helper without using paragraphs
928 931 def simple_format_without_paragraph(text)
929 932 text.to_s.
930 933 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
931 934 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
932 935 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
933 936 html_safe
934 937 end
935 938
936 939 def lang_options_for_select(blank=true)
937 940 (blank ? [["(auto)", ""]] : []) + languages_options
938 941 end
939 942
940 943 def label_tag_for(name, option_tags = nil, options = {})
941 944 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
942 945 content_tag("label", label_text)
943 946 end
944 947
945 948 def labelled_form_for(*args, &proc)
946 949 args << {} unless args.last.is_a?(Hash)
947 950 options = args.last
948 951 if args.first.is_a?(Symbol)
949 952 options.merge!(:as => args.shift)
950 953 end
951 954 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
952 955 form_for(*args, &proc)
953 956 end
954 957
955 958 def labelled_fields_for(*args, &proc)
956 959 args << {} unless args.last.is_a?(Hash)
957 960 options = args.last
958 961 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
959 962 fields_for(*args, &proc)
960 963 end
961 964
962 965 def labelled_remote_form_for(*args, &proc)
963 966 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_remote_form_for is deprecated and will be removed in Redmine 2.2."
964 967 args << {} unless args.last.is_a?(Hash)
965 968 options = args.last
966 969 options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
967 970 form_for(*args, &proc)
968 971 end
969 972
970 973 def error_messages_for(*objects)
971 974 html = ""
972 975 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
973 976 errors = objects.map {|o| o.errors.full_messages}.flatten
974 977 if errors.any?
975 978 html << "<div id='errorExplanation'><ul>\n"
976 979 errors.each do |error|
977 980 html << "<li>#{h error}</li>\n"
978 981 end
979 982 html << "</ul></div>\n"
980 983 end
981 984 html.html_safe
982 985 end
983 986
984 987 def delete_link(url, options={})
985 988 options = {
986 989 :method => :delete,
987 990 :data => {:confirm => l(:text_are_you_sure)},
988 991 :class => 'icon icon-del'
989 992 }.merge(options)
990 993
991 994 link_to l(:button_delete), url, options
992 995 end
993 996
994 997 def preview_link(url, form, target='preview', options={})
995 998 content_tag 'a', l(:label_preview), {
996 999 :href => "#",
997 1000 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
998 1001 :accesskey => accesskey(:preview)
999 1002 }.merge(options)
1000 1003 end
1001 1004
1002 1005 def link_to_function(name, function, html_options={})
1003 1006 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1004 1007 end
1005 1008
1006 1009 # Helper to render JSON in views
1007 1010 def raw_json(arg)
1008 1011 arg.to_json.to_s.gsub('/', '\/').html_safe
1009 1012 end
1010 1013
1011 1014 def back_url
1012 1015 url = params[:back_url]
1013 1016 if url.nil? && referer = request.env['HTTP_REFERER']
1014 1017 url = CGI.unescape(referer.to_s)
1015 1018 end
1016 1019 url
1017 1020 end
1018 1021
1019 1022 def back_url_hidden_field_tag
1020 1023 url = back_url
1021 1024 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1022 1025 end
1023 1026
1024 1027 def check_all_links(form_name)
1025 1028 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1026 1029 " | ".html_safe +
1027 1030 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1028 1031 end
1029 1032
1030 1033 def progress_bar(pcts, options={})
1031 1034 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1032 1035 pcts = pcts.collect(&:round)
1033 1036 pcts[1] = pcts[1] - pcts[0]
1034 1037 pcts << (100 - pcts[1] - pcts[0])
1035 1038 width = options[:width] || '100px;'
1036 1039 legend = options[:legend] || ''
1037 1040 content_tag('table',
1038 1041 content_tag('tr',
1039 1042 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1040 1043 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1041 1044 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1042 1045 ), :class => 'progress', :style => "width: #{width};").html_safe +
1043 1046 content_tag('p', legend, :class => 'percent').html_safe
1044 1047 end
1045 1048
1046 1049 def checked_image(checked=true)
1047 1050 if checked
1048 1051 image_tag 'toggle_check.png'
1049 1052 end
1050 1053 end
1051 1054
1052 1055 def context_menu(url)
1053 1056 unless @context_menu_included
1054 1057 content_for :header_tags do
1055 1058 javascript_include_tag('context_menu') +
1056 1059 stylesheet_link_tag('context_menu')
1057 1060 end
1058 1061 if l(:direction) == 'rtl'
1059 1062 content_for :header_tags do
1060 1063 stylesheet_link_tag('context_menu_rtl')
1061 1064 end
1062 1065 end
1063 1066 @context_menu_included = true
1064 1067 end
1065 1068 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1066 1069 end
1067 1070
1068 1071 def calendar_for(field_id)
1069 1072 include_calendar_headers_tags
1070 1073 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1071 1074 end
1072 1075
1073 1076 def include_calendar_headers_tags
1074 1077 unless @calendar_headers_tags_included
1075 1078 @calendar_headers_tags_included = true
1076 1079 content_for :header_tags do
1077 1080 start_of_week = Setting.start_of_week
1078 1081 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1079 1082 # Redmine uses 1..7 (monday..sunday) in settings and locales
1080 1083 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1081 1084 start_of_week = start_of_week.to_i % 7
1082 1085
1083 1086 tags = javascript_tag(
1084 1087 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1085 1088 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1086 1089 path_to_image('/images/calendar.png') +
1087 1090 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, selectOtherMonths: true, changeMonth: true, changeYear: true};")
1088 1091 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1089 1092 unless jquery_locale == 'en'
1090 1093 tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js")
1091 1094 end
1092 1095 tags
1093 1096 end
1094 1097 end
1095 1098 end
1096 1099
1097 1100 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1098 1101 # Examples:
1099 1102 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1100 1103 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1101 1104 #
1102 1105 def stylesheet_link_tag(*sources)
1103 1106 options = sources.last.is_a?(Hash) ? sources.pop : {}
1104 1107 plugin = options.delete(:plugin)
1105 1108 sources = sources.map do |source|
1106 1109 if plugin
1107 1110 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1108 1111 elsif current_theme && current_theme.stylesheets.include?(source)
1109 1112 current_theme.stylesheet_path(source)
1110 1113 else
1111 1114 source
1112 1115 end
1113 1116 end
1114 1117 super sources, options
1115 1118 end
1116 1119
1117 1120 # Overrides Rails' image_tag with themes and plugins support.
1118 1121 # Examples:
1119 1122 # image_tag('image.png') # => picks image.png from the current theme or defaults
1120 1123 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1121 1124 #
1122 1125 def image_tag(source, options={})
1123 1126 if plugin = options.delete(:plugin)
1124 1127 source = "/plugin_assets/#{plugin}/images/#{source}"
1125 1128 elsif current_theme && current_theme.images.include?(source)
1126 1129 source = current_theme.image_path(source)
1127 1130 end
1128 1131 super source, options
1129 1132 end
1130 1133
1131 1134 # Overrides Rails' javascript_include_tag with plugins support
1132 1135 # Examples:
1133 1136 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1134 1137 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1135 1138 #
1136 1139 def javascript_include_tag(*sources)
1137 1140 options = sources.last.is_a?(Hash) ? sources.pop : {}
1138 1141 if plugin = options.delete(:plugin)
1139 1142 sources = sources.map do |source|
1140 1143 if plugin
1141 1144 "/plugin_assets/#{plugin}/javascripts/#{source}"
1142 1145 else
1143 1146 source
1144 1147 end
1145 1148 end
1146 1149 end
1147 1150 super sources, options
1148 1151 end
1149 1152
1150 1153 def content_for(name, content = nil, &block)
1151 1154 @has_content ||= {}
1152 1155 @has_content[name] = true
1153 1156 super(name, content, &block)
1154 1157 end
1155 1158
1156 1159 def has_content?(name)
1157 1160 (@has_content && @has_content[name]) || false
1158 1161 end
1159 1162
1160 1163 def sidebar_content?
1161 1164 has_content?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1162 1165 end
1163 1166
1164 1167 def view_layouts_base_sidebar_hook_response
1165 1168 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1166 1169 end
1167 1170
1168 1171 def email_delivery_enabled?
1169 1172 !!ActionMailer::Base.perform_deliveries
1170 1173 end
1171 1174
1172 1175 # Returns the avatar image tag for the given +user+ if avatars are enabled
1173 1176 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1174 1177 def avatar(user, options = { })
1175 1178 if Setting.gravatar_enabled?
1176 1179 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1177 1180 email = nil
1178 1181 if user.respond_to?(:mail)
1179 1182 email = user.mail
1180 1183 elsif user.to_s =~ %r{<(.+?)>}
1181 1184 email = $1
1182 1185 end
1183 1186 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1184 1187 else
1185 1188 ''
1186 1189 end
1187 1190 end
1188 1191
1189 1192 def sanitize_anchor_name(anchor)
1190 1193 if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java'
1191 1194 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1192 1195 else
1193 1196 # TODO: remove when ruby1.8 is no longer supported
1194 1197 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1195 1198 end
1196 1199 end
1197 1200
1198 1201 # Returns the javascript tags that are included in the html layout head
1199 1202 def javascript_heads
1200 1203 tags = javascript_include_tag('jquery-1.8.3-ui-1.9.2-ujs-2.0.3', 'application')
1201 1204 unless User.current.pref.warn_on_leaving_unsaved == '0'
1202 1205 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1203 1206 end
1204 1207 tags
1205 1208 end
1206 1209
1207 1210 def favicon
1208 1211 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1209 1212 end
1210 1213
1211 1214 def robot_exclusion_tag
1212 1215 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1213 1216 end
1214 1217
1215 1218 # Returns true if arg is expected in the API response
1216 1219 def include_in_api_response?(arg)
1217 1220 unless @included_in_api_response
1218 1221 param = params[:include]
1219 1222 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1220 1223 @included_in_api_response.collect!(&:strip)
1221 1224 end
1222 1225 @included_in_api_response.include?(arg.to_s)
1223 1226 end
1224 1227
1225 1228 # Returns options or nil if nometa param or X-Redmine-Nometa header
1226 1229 # was set in the request
1227 1230 def api_meta(options)
1228 1231 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1229 1232 # compatibility mode for activeresource clients that raise
1230 1233 # an error when unserializing an array with attributes
1231 1234 nil
1232 1235 else
1233 1236 options
1234 1237 end
1235 1238 end
1236 1239
1237 1240 private
1238 1241
1239 1242 def wiki_helper
1240 1243 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1241 1244 extend helper
1242 1245 return self
1243 1246 end
1244 1247
1245 1248 def link_to_content_update(text, url_params = {}, html_options = {})
1246 1249 link_to(text, url_params, html_options)
1247 1250 end
1248 1251 end
General Comments 0
You need to be logged in to leave comments. Login now