##// END OF EJS Templates
Don't prepend project name if the version is not shared....
Jean-Philippe Lang -
r12972:2e04614e218e
parent child
Show More

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

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