##// END OF EJS Templates
Merged r14108 (#19348)....
Jean-Philippe Lang -
r13728:7783d7557fdb
parent child
Show More

The requested changes are too big and content was truncated. Show full diff

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