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