Mercurial > hg > soundsoftware-site
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 ? ' ' * 2 * level + '» ' : '').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\">¶</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 |