comparison .svn/pristine/fb/fb44c6e118ba1da9fa2fdfb5c015150a0c9a67d4.svn-base @ 1295:622f24f53b42 redmine-2.3

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