##// END OF EJS Templates
Merged r12415 (#15677)....
Jean-Philippe Lang -
r12157:fa2b39bf7317
parent child
Show More
@@ -1,1269 +1,1270
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 only_path = options[:only_path].nil? ? true : options[:only_path]
83 s = link_to text, issue_path(issue, :only_path => only_path), :class => issue.css_classes, :title => title
83 s << h(": #{subject}") if subject
84 s << h(": #{subject}") if subject
84 s = h("#{issue.project} - ") + s if options[:project]
85 s = h("#{issue.project} - ") + s if options[:project]
85 s
86 s
86 end
87 end
87
88
88 # Generates a link to an attachment.
89 # Generates a link to an attachment.
89 # Options:
90 # Options:
90 # * :text - Link text (default to attachment filename)
91 # * :text - Link text (default to attachment filename)
91 # * :download - Force download (default: false)
92 # * :download - Force download (default: false)
92 def link_to_attachment(attachment, options={})
93 def link_to_attachment(attachment, options={})
93 text = options.delete(:text) || attachment.filename
94 text = options.delete(:text) || attachment.filename
94 route_method = options.delete(:download) ? :download_named_attachment_path : :named_attachment_path
95 route_method = options.delete(:download) ? :download_named_attachment_path : :named_attachment_path
95 html_options = options.slice!(:only_path)
96 html_options = options.slice!(:only_path)
96 url = send(route_method, attachment, attachment.filename, options)
97 url = send(route_method, attachment, attachment.filename, options)
97 link_to text, url, html_options
98 link_to text, url, html_options
98 end
99 end
99
100
100 # Generates a link to a SCM revision
101 # Generates a link to a SCM revision
101 # Options:
102 # Options:
102 # * :text - Link text (default to the formatted revision)
103 # * :text - Link text (default to the formatted revision)
103 def link_to_revision(revision, repository, options={})
104 def link_to_revision(revision, repository, options={})
104 if repository.is_a?(Project)
105 if repository.is_a?(Project)
105 repository = repository.repository
106 repository = repository.repository
106 end
107 end
107 text = options.delete(:text) || format_revision(revision)
108 text = options.delete(:text) || format_revision(revision)
108 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
109 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
109 link_to(
110 link_to(
110 h(text),
111 h(text),
111 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
112 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
112 :title => l(:label_revision_id, format_revision(revision))
113 :title => l(:label_revision_id, format_revision(revision))
113 )
114 )
114 end
115 end
115
116
116 # Generates a link to a message
117 # Generates a link to a message
117 def link_to_message(message, options={}, html_options = nil)
118 def link_to_message(message, options={}, html_options = nil)
118 link_to(
119 link_to(
119 truncate(message.subject, :length => 60),
120 truncate(message.subject, :length => 60),
120 board_message_path(message.board_id, message.parent_id || message.id, {
121 board_message_path(message.board_id, message.parent_id || message.id, {
121 :r => (message.parent_id && message.id),
122 :r => (message.parent_id && message.id),
122 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
123 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
123 }.merge(options)),
124 }.merge(options)),
124 html_options
125 html_options
125 )
126 )
126 end
127 end
127
128
128 # Generates a link to a project if active
129 # Generates a link to a project if active
129 # Examples:
130 # Examples:
130 #
131 #
131 # link_to_project(project) # => link to the specified project overview
132 # link_to_project(project) # => link to the specified project overview
132 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
133 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
133 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
134 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
134 #
135 #
135 def link_to_project(project, options={}, html_options = nil)
136 def link_to_project(project, options={}, html_options = nil)
136 if project.archived?
137 if project.archived?
137 h(project.name)
138 h(project.name)
138 elsif options.key?(:action)
139 elsif options.key?(:action)
139 ActiveSupport::Deprecation.warn "#link_to_project with :action option is deprecated and will be removed in Redmine 3.0."
140 ActiveSupport::Deprecation.warn "#link_to_project with :action option is deprecated and will be removed in Redmine 3.0."
140 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
141 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
141 link_to project.name, url, html_options
142 link_to project.name, url, html_options
142 else
143 else
143 link_to project.name, project_path(project, options), html_options
144 link_to project.name, project_path(project, options), html_options
144 end
145 end
145 end
146 end
146
147
147 # Generates a link to a project settings if active
148 # Generates a link to a project settings if active
148 def link_to_project_settings(project, options={}, html_options=nil)
149 def link_to_project_settings(project, options={}, html_options=nil)
149 if project.active?
150 if project.active?
150 link_to project.name, settings_project_path(project, options), html_options
151 link_to project.name, settings_project_path(project, options), html_options
151 elsif project.archived?
152 elsif project.archived?
152 h(project.name)
153 h(project.name)
153 else
154 else
154 link_to project.name, project_path(project, options), html_options
155 link_to project.name, project_path(project, options), html_options
155 end
156 end
156 end
157 end
157
158
158 def wiki_page_path(page, options={})
159 def wiki_page_path(page, options={})
159 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
160 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
160 end
161 end
161
162
162 def thumbnail_tag(attachment)
163 def thumbnail_tag(attachment)
163 link_to image_tag(thumbnail_path(attachment)),
164 link_to image_tag(thumbnail_path(attachment)),
164 named_attachment_path(attachment, attachment.filename),
165 named_attachment_path(attachment, attachment.filename),
165 :title => attachment.filename
166 :title => attachment.filename
166 end
167 end
167
168
168 def toggle_link(name, id, options={})
169 def toggle_link(name, id, options={})
169 onclick = "$('##{id}').toggle(); "
170 onclick = "$('##{id}').toggle(); "
170 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
171 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
171 onclick << "return false;"
172 onclick << "return false;"
172 link_to(name, "#", :onclick => onclick)
173 link_to(name, "#", :onclick => onclick)
173 end
174 end
174
175
175 def image_to_function(name, function, html_options = {})
176 def image_to_function(name, function, html_options = {})
176 html_options.symbolize_keys!
177 html_options.symbolize_keys!
177 tag(:input, html_options.merge({
178 tag(:input, html_options.merge({
178 :type => "image", :src => image_path(name),
179 :type => "image", :src => image_path(name),
179 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
180 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
180 }))
181 }))
181 end
182 end
182
183
183 def format_activity_title(text)
184 def format_activity_title(text)
184 h(truncate_single_line(text, :length => 100))
185 h(truncate_single_line(text, :length => 100))
185 end
186 end
186
187
187 def format_activity_day(date)
188 def format_activity_day(date)
188 date == User.current.today ? l(:label_today).titleize : format_date(date)
189 date == User.current.today ? l(:label_today).titleize : format_date(date)
189 end
190 end
190
191
191 def format_activity_description(text)
192 def format_activity_description(text)
192 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
193 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
193 ).gsub(/[\r\n]+/, "<br />").html_safe
194 ).gsub(/[\r\n]+/, "<br />").html_safe
194 end
195 end
195
196
196 def format_version_name(version)
197 def format_version_name(version)
197 if version.project == @project
198 if version.project == @project
198 h(version)
199 h(version)
199 else
200 else
200 h("#{version.project} - #{version}")
201 h("#{version.project} - #{version}")
201 end
202 end
202 end
203 end
203
204
204 def due_date_distance_in_words(date)
205 def due_date_distance_in_words(date)
205 if date
206 if date
206 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
207 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
207 end
208 end
208 end
209 end
209
210
210 # Renders a tree of projects as a nested set of unordered lists
211 # Renders a tree of projects as a nested set of unordered lists
211 # The given collection may be a subset of the whole project tree
212 # The given collection may be a subset of the whole project tree
212 # (eg. some intermediate nodes are private and can not be seen)
213 # (eg. some intermediate nodes are private and can not be seen)
213 def render_project_nested_lists(projects)
214 def render_project_nested_lists(projects)
214 s = ''
215 s = ''
215 if projects.any?
216 if projects.any?
216 ancestors = []
217 ancestors = []
217 original_project = @project
218 original_project = @project
218 projects.sort_by(&:lft).each do |project|
219 projects.sort_by(&:lft).each do |project|
219 # set the project environment to please macros.
220 # set the project environment to please macros.
220 @project = project
221 @project = project
221 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
222 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
222 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
223 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
223 else
224 else
224 ancestors.pop
225 ancestors.pop
225 s << "</li>"
226 s << "</li>"
226 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
227 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
227 ancestors.pop
228 ancestors.pop
228 s << "</ul></li>\n"
229 s << "</ul></li>\n"
229 end
230 end
230 end
231 end
231 classes = (ancestors.empty? ? 'root' : 'child')
232 classes = (ancestors.empty? ? 'root' : 'child')
232 s << "<li class='#{classes}'><div class='#{classes}'>"
233 s << "<li class='#{classes}'><div class='#{classes}'>"
233 s << h(block_given? ? yield(project) : project.name)
234 s << h(block_given? ? yield(project) : project.name)
234 s << "</div>\n"
235 s << "</div>\n"
235 ancestors << project
236 ancestors << project
236 end
237 end
237 s << ("</li></ul>\n" * ancestors.size)
238 s << ("</li></ul>\n" * ancestors.size)
238 @project = original_project
239 @project = original_project
239 end
240 end
240 s.html_safe
241 s.html_safe
241 end
242 end
242
243
243 def render_page_hierarchy(pages, node=nil, options={})
244 def render_page_hierarchy(pages, node=nil, options={})
244 content = ''
245 content = ''
245 if pages[node]
246 if pages[node]
246 content << "<ul class=\"pages-hierarchy\">\n"
247 content << "<ul class=\"pages-hierarchy\">\n"
247 pages[node].each do |page|
248 pages[node].each do |page|
248 content << "<li>"
249 content << "<li>"
249 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
250 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
250 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
251 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
251 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
252 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
252 content << "</li>\n"
253 content << "</li>\n"
253 end
254 end
254 content << "</ul>\n"
255 content << "</ul>\n"
255 end
256 end
256 content.html_safe
257 content.html_safe
257 end
258 end
258
259
259 # Renders flash messages
260 # Renders flash messages
260 def render_flash_messages
261 def render_flash_messages
261 s = ''
262 s = ''
262 flash.each do |k,v|
263 flash.each do |k,v|
263 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
264 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
264 end
265 end
265 s.html_safe
266 s.html_safe
266 end
267 end
267
268
268 # Renders tabs and their content
269 # Renders tabs and their content
269 def render_tabs(tabs)
270 def render_tabs(tabs)
270 if tabs.any?
271 if tabs.any?
271 render :partial => 'common/tabs', :locals => {:tabs => tabs}
272 render :partial => 'common/tabs', :locals => {:tabs => tabs}
272 else
273 else
273 content_tag 'p', l(:label_no_data), :class => "nodata"
274 content_tag 'p', l(:label_no_data), :class => "nodata"
274 end
275 end
275 end
276 end
276
277
277 # Renders the project quick-jump box
278 # Renders the project quick-jump box
278 def render_project_jump_box
279 def render_project_jump_box
279 return unless User.current.logged?
280 return unless User.current.logged?
280 projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
281 projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
281 if projects.any?
282 if projects.any?
282 options =
283 options =
283 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
284 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
284 '<option value="" disabled="disabled">---</option>').html_safe
285 '<option value="" disabled="disabled">---</option>').html_safe
285
286
286 options << project_tree_options_for_select(projects, :selected => @project) do |p|
287 options << project_tree_options_for_select(projects, :selected => @project) do |p|
287 { :value => project_path(:id => p, :jump => current_menu_item) }
288 { :value => project_path(:id => p, :jump => current_menu_item) }
288 end
289 end
289
290
290 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
291 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
291 end
292 end
292 end
293 end
293
294
294 def project_tree_options_for_select(projects, options = {})
295 def project_tree_options_for_select(projects, options = {})
295 s = ''
296 s = ''
296 project_tree(projects) do |project, level|
297 project_tree(projects) do |project, level|
297 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
298 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
298 tag_options = {:value => project.id}
299 tag_options = {:value => project.id}
299 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
300 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
300 tag_options[:selected] = 'selected'
301 tag_options[:selected] = 'selected'
301 else
302 else
302 tag_options[:selected] = nil
303 tag_options[:selected] = nil
303 end
304 end
304 tag_options.merge!(yield(project)) if block_given?
305 tag_options.merge!(yield(project)) if block_given?
305 s << content_tag('option', name_prefix + h(project), tag_options)
306 s << content_tag('option', name_prefix + h(project), tag_options)
306 end
307 end
307 s.html_safe
308 s.html_safe
308 end
309 end
309
310
310 # Yields the given block for each project with its level in the tree
311 # Yields the given block for each project with its level in the tree
311 #
312 #
312 # Wrapper for Project#project_tree
313 # Wrapper for Project#project_tree
313 def project_tree(projects, &block)
314 def project_tree(projects, &block)
314 Project.project_tree(projects, &block)
315 Project.project_tree(projects, &block)
315 end
316 end
316
317
317 def principals_check_box_tags(name, principals)
318 def principals_check_box_tags(name, principals)
318 s = ''
319 s = ''
319 principals.each do |principal|
320 principals.each do |principal|
320 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
321 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
321 end
322 end
322 s.html_safe
323 s.html_safe
323 end
324 end
324
325
325 # Returns a string for users/groups option tags
326 # Returns a string for users/groups option tags
326 def principals_options_for_select(collection, selected=nil)
327 def principals_options_for_select(collection, selected=nil)
327 s = ''
328 s = ''
328 if collection.include?(User.current)
329 if collection.include?(User.current)
329 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
330 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
330 end
331 end
331 groups = ''
332 groups = ''
332 collection.sort.each do |element|
333 collection.sort.each do |element|
333 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
334 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>)
335 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
335 end
336 end
336 unless groups.empty?
337 unless groups.empty?
337 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
338 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
338 end
339 end
339 s.html_safe
340 s.html_safe
340 end
341 end
341
342
342 # Options for the new membership projects combo-box
343 # Options for the new membership projects combo-box
343 def options_for_membership_project_select(principal, projects)
344 def options_for_membership_project_select(principal, projects)
344 options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
345 options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
345 options << project_tree_options_for_select(projects) do |p|
346 options << project_tree_options_for_select(projects) do |p|
346 {:disabled => principal.projects.to_a.include?(p)}
347 {:disabled => principal.projects.to_a.include?(p)}
347 end
348 end
348 options
349 options
349 end
350 end
350
351
351 def option_tag(name, text, value, selected=nil, options={})
352 def option_tag(name, text, value, selected=nil, options={})
352 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
353 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
353 end
354 end
354
355
355 # Truncates and returns the string as a single line
356 # Truncates and returns the string as a single line
356 def truncate_single_line(string, *args)
357 def truncate_single_line(string, *args)
357 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
358 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
358 end
359 end
359
360
360 # Truncates at line break after 250 characters or options[:length]
361 # Truncates at line break after 250 characters or options[:length]
361 def truncate_lines(string, options={})
362 def truncate_lines(string, options={})
362 length = options[:length] || 250
363 length = options[:length] || 250
363 if string.to_s =~ /\A(.{#{length}}.*?)$/m
364 if string.to_s =~ /\A(.{#{length}}.*?)$/m
364 "#{$1}..."
365 "#{$1}..."
365 else
366 else
366 string
367 string
367 end
368 end
368 end
369 end
369
370
370 def anchor(text)
371 def anchor(text)
371 text.to_s.gsub(' ', '_')
372 text.to_s.gsub(' ', '_')
372 end
373 end
373
374
374 def html_hours(text)
375 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
376 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
376 end
377 end
377
378
378 def authoring(created, author, options={})
379 def authoring(created, author, options={})
379 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
380 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
380 end
381 end
381
382
382 def time_tag(time)
383 def time_tag(time)
383 text = distance_of_time_in_words(Time.now, time)
384 text = distance_of_time_in_words(Time.now, time)
384 if @project
385 if @project
385 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
386 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
386 else
387 else
387 content_tag('abbr', text, :title => format_time(time))
388 content_tag('abbr', text, :title => format_time(time))
388 end
389 end
389 end
390 end
390
391
391 def syntax_highlight_lines(name, content)
392 def syntax_highlight_lines(name, content)
392 lines = []
393 lines = []
393 syntax_highlight(name, content).each_line { |line| lines << line }
394 syntax_highlight(name, content).each_line { |line| lines << line }
394 lines
395 lines
395 end
396 end
396
397
397 def syntax_highlight(name, content)
398 def syntax_highlight(name, content)
398 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
399 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
399 end
400 end
400
401
401 def to_path_param(path)
402 def to_path_param(path)
402 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
403 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
403 str.blank? ? nil : str
404 str.blank? ? nil : str
404 end
405 end
405
406
406 def reorder_links(name, url, method = :post)
407 def reorder_links(name, url, method = :post)
407 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
408 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
408 url.merge({"#{name}[move_to]" => 'highest'}),
409 url.merge({"#{name}[move_to]" => 'highest'}),
409 :method => method, :title => l(:label_sort_highest)) +
410 :method => method, :title => l(:label_sort_highest)) +
410 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
411 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
411 url.merge({"#{name}[move_to]" => 'higher'}),
412 url.merge({"#{name}[move_to]" => 'higher'}),
412 :method => method, :title => l(:label_sort_higher)) +
413 :method => method, :title => l(:label_sort_higher)) +
413 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
414 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
414 url.merge({"#{name}[move_to]" => 'lower'}),
415 url.merge({"#{name}[move_to]" => 'lower'}),
415 :method => method, :title => l(:label_sort_lower)) +
416 :method => method, :title => l(:label_sort_lower)) +
416 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
417 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
417 url.merge({"#{name}[move_to]" => 'lowest'}),
418 url.merge({"#{name}[move_to]" => 'lowest'}),
418 :method => method, :title => l(:label_sort_lowest))
419 :method => method, :title => l(:label_sort_lowest))
419 end
420 end
420
421
421 def breadcrumb(*args)
422 def breadcrumb(*args)
422 elements = args.flatten
423 elements = args.flatten
423 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
424 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
424 end
425 end
425
426
426 def other_formats_links(&block)
427 def other_formats_links(&block)
427 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
428 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
428 yield Redmine::Views::OtherFormatsBuilder.new(self)
429 yield Redmine::Views::OtherFormatsBuilder.new(self)
429 concat('</p>'.html_safe)
430 concat('</p>'.html_safe)
430 end
431 end
431
432
432 def page_header_title
433 def page_header_title
433 if @project.nil? || @project.new_record?
434 if @project.nil? || @project.new_record?
434 h(Setting.app_title)
435 h(Setting.app_title)
435 else
436 else
436 b = []
437 b = []
437 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
438 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
438 if ancestors.any?
439 if ancestors.any?
439 root = ancestors.shift
440 root = ancestors.shift
440 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
441 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
441 if ancestors.size > 2
442 if ancestors.size > 2
442 b << "\xe2\x80\xa6"
443 b << "\xe2\x80\xa6"
443 ancestors = ancestors[-2, 2]
444 ancestors = ancestors[-2, 2]
444 end
445 end
445 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
446 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
446 end
447 end
447 b << h(@project)
448 b << h(@project)
448 b.join(" \xc2\xbb ").html_safe
449 b.join(" \xc2\xbb ").html_safe
449 end
450 end
450 end
451 end
451
452
452 # Returns a h2 tag and sets the html title with the given arguments
453 # Returns a h2 tag and sets the html title with the given arguments
453 def title(*args)
454 def title(*args)
454 strings = args.map do |arg|
455 strings = args.map do |arg|
455 if arg.is_a?(Array) && arg.size >= 2
456 if arg.is_a?(Array) && arg.size >= 2
456 link_to(*arg)
457 link_to(*arg)
457 else
458 else
458 h(arg.to_s)
459 h(arg.to_s)
459 end
460 end
460 end
461 end
461 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
462 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
462 content_tag('h2', strings.join(' &#187; ').html_safe)
463 content_tag('h2', strings.join(' &#187; ').html_safe)
463 end
464 end
464
465
465 # Sets the html title
466 # Sets the html title
466 # Returns the html title when called without arguments
467 # Returns the html title when called without arguments
467 # Current project name and app_title and automatically appended
468 # Current project name and app_title and automatically appended
468 # Exemples:
469 # Exemples:
469 # html_title 'Foo', 'Bar'
470 # html_title 'Foo', 'Bar'
470 # html_title # => 'Foo - Bar - My Project - Redmine'
471 # html_title # => 'Foo - Bar - My Project - Redmine'
471 def html_title(*args)
472 def html_title(*args)
472 if args.empty?
473 if args.empty?
473 title = @html_title || []
474 title = @html_title || []
474 title << @project.name if @project
475 title << @project.name if @project
475 title << Setting.app_title unless Setting.app_title == title.last
476 title << Setting.app_title unless Setting.app_title == title.last
476 title.reject(&:blank?).join(' - ')
477 title.reject(&:blank?).join(' - ')
477 else
478 else
478 @html_title ||= []
479 @html_title ||= []
479 @html_title += args
480 @html_title += args
480 end
481 end
481 end
482 end
482
483
483 # Returns the theme, controller name, and action as css classes for the
484 # Returns the theme, controller name, and action as css classes for the
484 # HTML body.
485 # HTML body.
485 def body_css_classes
486 def body_css_classes
486 css = []
487 css = []
487 if theme = Redmine::Themes.theme(Setting.ui_theme)
488 if theme = Redmine::Themes.theme(Setting.ui_theme)
488 css << 'theme-' + theme.name
489 css << 'theme-' + theme.name
489 end
490 end
490
491
491 css << 'project-' + @project.identifier if @project && @project.identifier.present?
492 css << 'project-' + @project.identifier if @project && @project.identifier.present?
492 css << 'controller-' + controller_name
493 css << 'controller-' + controller_name
493 css << 'action-' + action_name
494 css << 'action-' + action_name
494 css.join(' ')
495 css.join(' ')
495 end
496 end
496
497
497 def accesskey(s)
498 def accesskey(s)
498 @used_accesskeys ||= []
499 @used_accesskeys ||= []
499 key = Redmine::AccessKeys.key_for(s)
500 key = Redmine::AccessKeys.key_for(s)
500 return nil if @used_accesskeys.include?(key)
501 return nil if @used_accesskeys.include?(key)
501 @used_accesskeys << key
502 @used_accesskeys << key
502 key
503 key
503 end
504 end
504
505
505 # Formats text according to system settings.
506 # Formats text according to system settings.
506 # 2 ways to call this method:
507 # 2 ways to call this method:
507 # * with a String: textilizable(text, options)
508 # * with a String: textilizable(text, options)
508 # * with an object and one of its attribute: textilizable(issue, :description, options)
509 # * with an object and one of its attribute: textilizable(issue, :description, options)
509 def textilizable(*args)
510 def textilizable(*args)
510 options = args.last.is_a?(Hash) ? args.pop : {}
511 options = args.last.is_a?(Hash) ? args.pop : {}
511 case args.size
512 case args.size
512 when 1
513 when 1
513 obj = options[:object]
514 obj = options[:object]
514 text = args.shift
515 text = args.shift
515 when 2
516 when 2
516 obj = args.shift
517 obj = args.shift
517 attr = args.shift
518 attr = args.shift
518 text = obj.send(attr).to_s
519 text = obj.send(attr).to_s
519 else
520 else
520 raise ArgumentError, 'invalid arguments to textilizable'
521 raise ArgumentError, 'invalid arguments to textilizable'
521 end
522 end
522 return '' if text.blank?
523 return '' if text.blank?
523 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
524 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
524 only_path = options.delete(:only_path) == false ? false : true
525 only_path = options.delete(:only_path) == false ? false : true
525
526
526 text = text.dup
527 text = text.dup
527 macros = catch_macros(text)
528 macros = catch_macros(text)
528 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
529 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
529
530
530 @parsed_headings = []
531 @parsed_headings = []
531 @heading_anchors = {}
532 @heading_anchors = {}
532 @current_section = 0 if options[:edit_section_links]
533 @current_section = 0 if options[:edit_section_links]
533
534
534 parse_sections(text, project, obj, attr, only_path, options)
535 parse_sections(text, project, obj, attr, only_path, options)
535 text = parse_non_pre_blocks(text, obj, macros) do |text|
536 text = parse_non_pre_blocks(text, obj, macros) do |text|
536 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
537 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
537 send method_name, text, project, obj, attr, only_path, options
538 send method_name, text, project, obj, attr, only_path, options
538 end
539 end
539 end
540 end
540 parse_headings(text, project, obj, attr, only_path, options)
541 parse_headings(text, project, obj, attr, only_path, options)
541
542
542 if @parsed_headings.any?
543 if @parsed_headings.any?
543 replace_toc(text, @parsed_headings)
544 replace_toc(text, @parsed_headings)
544 end
545 end
545
546
546 text.html_safe
547 text.html_safe
547 end
548 end
548
549
549 def parse_non_pre_blocks(text, obj, macros)
550 def parse_non_pre_blocks(text, obj, macros)
550 s = StringScanner.new(text)
551 s = StringScanner.new(text)
551 tags = []
552 tags = []
552 parsed = ''
553 parsed = ''
553 while !s.eos?
554 while !s.eos?
554 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
555 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
555 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
556 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
556 if tags.empty?
557 if tags.empty?
557 yield text
558 yield text
558 inject_macros(text, obj, macros) if macros.any?
559 inject_macros(text, obj, macros) if macros.any?
559 else
560 else
560 inject_macros(text, obj, macros, false) if macros.any?
561 inject_macros(text, obj, macros, false) if macros.any?
561 end
562 end
562 parsed << text
563 parsed << text
563 if tag
564 if tag
564 if closing
565 if closing
565 if tags.last == tag.downcase
566 if tags.last == tag.downcase
566 tags.pop
567 tags.pop
567 end
568 end
568 else
569 else
569 tags << tag.downcase
570 tags << tag.downcase
570 end
571 end
571 parsed << full_tag
572 parsed << full_tag
572 end
573 end
573 end
574 end
574 # Close any non closing tags
575 # Close any non closing tags
575 while tag = tags.pop
576 while tag = tags.pop
576 parsed << "</#{tag}>"
577 parsed << "</#{tag}>"
577 end
578 end
578 parsed
579 parsed
579 end
580 end
580
581
581 def parse_inline_attachments(text, project, obj, attr, only_path, options)
582 def parse_inline_attachments(text, project, obj, attr, only_path, options)
582 # when using an image link, try to use an attachment, if possible
583 # when using an image link, try to use an attachment, if possible
583 attachments = options[:attachments] || []
584 attachments = options[:attachments] || []
584 attachments += obj.attachments if obj.respond_to?(:attachments)
585 attachments += obj.attachments if obj.respond_to?(:attachments)
585 if attachments.present?
586 if attachments.present?
586 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
587 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
587 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
588 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
588 # search for the picture in attachments
589 # search for the picture in attachments
589 if found = Attachment.latest_attach(attachments, filename)
590 if found = Attachment.latest_attach(attachments, filename)
590 image_url = download_named_attachment_path(found, found.filename, :only_path => only_path)
591 image_url = download_named_attachment_path(found, found.filename, :only_path => only_path)
591 desc = found.description.to_s.gsub('"', '')
592 desc = found.description.to_s.gsub('"', '')
592 if !desc.blank? && alttext.blank?
593 if !desc.blank? && alttext.blank?
593 alt = " title=\"#{desc}\" alt=\"#{desc}\""
594 alt = " title=\"#{desc}\" alt=\"#{desc}\""
594 end
595 end
595 "src=\"#{image_url}\"#{alt}"
596 "src=\"#{image_url}\"#{alt}"
596 else
597 else
597 m
598 m
598 end
599 end
599 end
600 end
600 end
601 end
601 end
602 end
602
603
603 # Wiki links
604 # Wiki links
604 #
605 #
605 # Examples:
606 # Examples:
606 # [[mypage]]
607 # [[mypage]]
607 # [[mypage|mytext]]
608 # [[mypage|mytext]]
608 # wiki links can refer other project wikis, using project name or identifier:
609 # wiki links can refer other project wikis, using project name or identifier:
609 # [[project:]] -> wiki starting page
610 # [[project:]] -> wiki starting page
610 # [[project:|mytext]]
611 # [[project:|mytext]]
611 # [[project:mypage]]
612 # [[project:mypage]]
612 # [[project:mypage|mytext]]
613 # [[project:mypage|mytext]]
613 def parse_wiki_links(text, project, obj, attr, only_path, options)
614 def parse_wiki_links(text, project, obj, attr, only_path, options)
614 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
615 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
615 link_project = project
616 link_project = project
616 esc, all, page, title = $1, $2, $3, $5
617 esc, all, page, title = $1, $2, $3, $5
617 if esc.nil?
618 if esc.nil?
618 if page =~ /^([^\:]+)\:(.*)$/
619 if page =~ /^([^\:]+)\:(.*)$/
619 identifier, page = $1, $2
620 identifier, page = $1, $2
620 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
621 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
621 title ||= identifier if page.blank?
622 title ||= identifier if page.blank?
622 end
623 end
623
624
624 if link_project && link_project.wiki
625 if link_project && link_project.wiki
625 # extract anchor
626 # extract anchor
626 anchor = nil
627 anchor = nil
627 if page =~ /^(.+?)\#(.+)$/
628 if page =~ /^(.+?)\#(.+)$/
628 page, anchor = $1, $2
629 page, anchor = $1, $2
629 end
630 end
630 anchor = sanitize_anchor_name(anchor) if anchor.present?
631 anchor = sanitize_anchor_name(anchor) if anchor.present?
631 # check if page exists
632 # check if page exists
632 wiki_page = link_project.wiki.find_page(page)
633 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
634 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
634 "##{anchor}"
635 "##{anchor}"
635 else
636 else
636 case options[:wiki_links]
637 case options[:wiki_links]
637 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
638 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
639 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
639 else
640 else
640 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
641 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
642 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,
643 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)
644 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
644 end
645 end
645 end
646 end
646 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
647 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
647 else
648 else
648 # project or wiki doesn't exist
649 # project or wiki doesn't exist
649 all
650 all
650 end
651 end
651 else
652 else
652 all
653 all
653 end
654 end
654 end
655 end
655 end
656 end
656
657
657 # Redmine links
658 # Redmine links
658 #
659 #
659 # Examples:
660 # Examples:
660 # Issues:
661 # Issues:
661 # #52 -> Link to issue #52
662 # #52 -> Link to issue #52
662 # Changesets:
663 # Changesets:
663 # r52 -> Link to revision 52
664 # r52 -> Link to revision 52
664 # commit:a85130f -> Link to scmid starting with a85130f
665 # commit:a85130f -> Link to scmid starting with a85130f
665 # Documents:
666 # Documents:
666 # document#17 -> Link to document with id 17
667 # document#17 -> Link to document with id 17
667 # document:Greetings -> Link to the document with title "Greetings"
668 # document:Greetings -> Link to the document with title "Greetings"
668 # document:"Some document" -> Link to the document with title "Some document"
669 # document:"Some document" -> Link to the document with title "Some document"
669 # Versions:
670 # Versions:
670 # version#3 -> Link to version with id 3
671 # version#3 -> Link to version with id 3
671 # version:1.0.0 -> Link to version named "1.0.0"
672 # 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"
673 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
673 # Attachments:
674 # Attachments:
674 # attachment:file.zip -> Link to the attachment of the current object named file.zip
675 # attachment:file.zip -> Link to the attachment of the current object named file.zip
675 # Source files:
676 # Source files:
676 # source:some/file -> Link to the file located at /some/file in the project's repository
677 # 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
678 # source:some/file@52 -> Link to the file's revision 52
678 # source:some/file#L120 -> Link to line 120 of the file
679 # 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
680 # 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
681 # export:some/file -> Force the download of the file
681 # Forum messages:
682 # Forum messages:
682 # message#1218 -> Link to message with id 1218
683 # message#1218 -> Link to message with id 1218
683 # Projects:
684 # Projects:
684 # project:someproject -> Link to project named "someproject"
685 # project:someproject -> Link to project named "someproject"
685 # project#3 -> Link to project with id 3
686 # project#3 -> Link to project with id 3
686 #
687 #
687 # Links can refer other objects from other projects, using project identifier:
688 # Links can refer other objects from other projects, using project identifier:
688 # identifier:r52
689 # identifier:r52
689 # identifier:document:"Some document"
690 # identifier:document:"Some document"
690 # identifier:version:1.0.0
691 # identifier:version:1.0.0
691 # identifier:source:some/file
692 # identifier:source:some/file
692 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
693 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|
694 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
695 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
696 link = nil
696 project = default_project
697 project = default_project
697 if project_identifier
698 if project_identifier
698 project = Project.visible.find_by_identifier(project_identifier)
699 project = Project.visible.find_by_identifier(project_identifier)
699 end
700 end
700 if esc.nil?
701 if esc.nil?
701 if prefix.nil? && sep == 'r'
702 if prefix.nil? && sep == 'r'
702 if project
703 if project
703 repository = nil
704 repository = nil
704 if repo_identifier
705 if repo_identifier
705 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
706 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
706 else
707 else
707 repository = project.repository
708 repository = project.repository
708 end
709 end
709 # project.changesets.visible raises an SQL error because of a double join on repositories
710 # 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))
711 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},
712 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',
713 :class => 'changeset',
713 :title => truncate_single_line(changeset.comments, :length => 100))
714 :title => truncate_single_line(changeset.comments, :length => 100))
714 end
715 end
715 end
716 end
716 elsif sep == '#'
717 elsif sep == '#'
717 oid = identifier.to_i
718 oid = identifier.to_i
718 case prefix
719 case prefix
719 when nil
720 when nil
720 if oid.to_s == identifier && issue = Issue.visible.find_by_id(oid, :include => :status)
721 if oid.to_s == identifier && issue = Issue.visible.find_by_id(oid, :include => :status)
721 anchor = comment_id ? "note-#{comment_id}" : nil
722 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},
723 link = link_to(h("##{oid}#{comment_suffix}"), {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor},
723 :class => issue.css_classes,
724 :class => issue.css_classes,
724 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
725 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
725 end
726 end
726 when 'document'
727 when 'document'
727 if document = Document.visible.find_by_id(oid)
728 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},
729 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
729 :class => 'document'
730 :class => 'document'
730 end
731 end
731 when 'version'
732 when 'version'
732 if version = Version.visible.find_by_id(oid)
733 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},
734 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
734 :class => 'version'
735 :class => 'version'
735 end
736 end
736 when 'message'
737 when 'message'
737 if message = Message.visible.find_by_id(oid, :include => :parent)
738 if message = Message.visible.find_by_id(oid, :include => :parent)
738 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
739 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
739 end
740 end
740 when 'forum'
741 when 'forum'
741 if board = Board.visible.find_by_id(oid)
742 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},
743 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
743 :class => 'board'
744 :class => 'board'
744 end
745 end
745 when 'news'
746 when 'news'
746 if news = News.visible.find_by_id(oid)
747 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},
748 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
748 :class => 'news'
749 :class => 'news'
749 end
750 end
750 when 'project'
751 when 'project'
751 if p = Project.visible.find_by_id(oid)
752 if p = Project.visible.find_by_id(oid)
752 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
753 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
753 end
754 end
754 end
755 end
755 elsif sep == ':'
756 elsif sep == ':'
756 # removes the double quotes if any
757 # removes the double quotes if any
757 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
758 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
758 case prefix
759 case prefix
759 when 'document'
760 when 'document'
760 if project && document = project.documents.visible.find_by_title(name)
761 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},
762 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
762 :class => 'document'
763 :class => 'document'
763 end
764 end
764 when 'version'
765 when 'version'
765 if project && version = project.versions.visible.find_by_name(name)
766 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},
767 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
767 :class => 'version'
768 :class => 'version'
768 end
769 end
769 when 'forum'
770 when 'forum'
770 if project && board = project.boards.visible.find_by_name(name)
771 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},
772 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
772 :class => 'board'
773 :class => 'board'
773 end
774 end
774 when 'news'
775 when 'news'
775 if project && news = project.news.visible.find_by_title(name)
776 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},
777 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
777 :class => 'news'
778 :class => 'news'
778 end
779 end
779 when 'commit', 'source', 'export'
780 when 'commit', 'source', 'export'
780 if project
781 if project
781 repository = nil
782 repository = nil
782 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
783 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
783 repo_prefix, repo_identifier, name = $1, $2, $3
784 repo_prefix, repo_identifier, name = $1, $2, $3
784 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
785 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
785 else
786 else
786 repository = project.repository
787 repository = project.repository
787 end
788 end
788 if prefix == 'commit'
789 if prefix == 'commit'
789 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
790 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},
791 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',
792 :class => 'changeset',
792 :title => truncate_single_line(changeset.comments, :length => 100)
793 :title => truncate_single_line(changeset.comments, :length => 100)
793 end
794 end
794 else
795 else
795 if repository && User.current.allowed_to?(:browse_repository, project)
796 if repository && User.current.allowed_to?(:browse_repository, project)
796 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
797 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
797 path, rev, anchor = $1, $3, $5
798 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,
799 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),
800 :path => to_path_param(path),
800 :rev => rev,
801 :rev => rev,
801 :anchor => anchor},
802 :anchor => anchor},
802 :class => (prefix == 'export' ? 'source download' : 'source')
803 :class => (prefix == 'export' ? 'source download' : 'source')
803 end
804 end
804 end
805 end
805 repo_prefix = nil
806 repo_prefix = nil
806 end
807 end
807 when 'attachment'
808 when 'attachment'
808 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
809 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
809 if attachments && attachment = Attachment.latest_attach(attachments, name)
810 if attachments && attachment = Attachment.latest_attach(attachments, name)
810 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
811 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
811 end
812 end
812 when 'project'
813 when 'project'
813 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
814 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')
815 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
815 end
816 end
816 end
817 end
817 end
818 end
818 end
819 end
819 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
820 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
820 end
821 end
821 end
822 end
822
823
823 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
824 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
824
825
825 def parse_sections(text, project, obj, attr, only_path, options)
826 def parse_sections(text, project, obj, attr, only_path, options)
826 return unless options[:edit_section_links]
827 return unless options[:edit_section_links]
827 text.gsub!(HEADING_RE) do
828 text.gsub!(HEADING_RE) do
828 heading = $1
829 heading = $1
829 @current_section += 1
830 @current_section += 1
830 if @current_section > 1
831 if @current_section > 1
831 content_tag('div',
832 content_tag('div',
832 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
833 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
833 :class => 'contextual',
834 :class => 'contextual',
834 :title => l(:button_edit_section),
835 :title => l(:button_edit_section),
835 :id => "section-#{@current_section}") + heading.html_safe
836 :id => "section-#{@current_section}") + heading.html_safe
836 else
837 else
837 heading
838 heading
838 end
839 end
839 end
840 end
840 end
841 end
841
842
842 # Headings and TOC
843 # Headings and TOC
843 # Adds ids and links to headings unless options[:headings] is set to false
844 # Adds ids and links to headings unless options[:headings] is set to false
844 def parse_headings(text, project, obj, attr, only_path, options)
845 def parse_headings(text, project, obj, attr, only_path, options)
845 return if options[:headings] == false
846 return if options[:headings] == false
846
847
847 text.gsub!(HEADING_RE) do
848 text.gsub!(HEADING_RE) do
848 level, attrs, content = $2.to_i, $3, $4
849 level, attrs, content = $2.to_i, $3, $4
849 item = strip_tags(content).strip
850 item = strip_tags(content).strip
850 anchor = sanitize_anchor_name(item)
851 anchor = sanitize_anchor_name(item)
851 # used for single-file wiki export
852 # 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))
853 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
853 @heading_anchors[anchor] ||= 0
854 @heading_anchors[anchor] ||= 0
854 idx = (@heading_anchors[anchor] += 1)
855 idx = (@heading_anchors[anchor] += 1)
855 if idx > 1
856 if idx > 1
856 anchor = "#{anchor}-#{idx}"
857 anchor = "#{anchor}-#{idx}"
857 end
858 end
858 @parsed_headings << [level, anchor, item]
859 @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}>"
860 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
860 end
861 end
861 end
862 end
862
863
863 MACROS_RE = /(
864 MACROS_RE = /(
864 (!)? # escaping
865 (!)? # escaping
865 (
866 (
866 \{\{ # opening tag
867 \{\{ # opening tag
867 ([\w]+) # macro name
868 ([\w]+) # macro name
868 (\(([^\n\r]*?)\))? # optional arguments
869 (\(([^\n\r]*?)\))? # optional arguments
869 ([\n\r].*?[\n\r])? # optional block of text
870 ([\n\r].*?[\n\r])? # optional block of text
870 \}\} # closing tag
871 \}\} # closing tag
871 )
872 )
872 )/mx unless const_defined?(:MACROS_RE)
873 )/mx unless const_defined?(:MACROS_RE)
873
874
874 MACRO_SUB_RE = /(
875 MACRO_SUB_RE = /(
875 \{\{
876 \{\{
876 macro\((\d+)\)
877 macro\((\d+)\)
877 \}\}
878 \}\}
878 )/x unless const_defined?(:MACRO_SUB_RE)
879 )/x unless const_defined?(:MACRO_SUB_RE)
879
880
880 # Extracts macros from text
881 # Extracts macros from text
881 def catch_macros(text)
882 def catch_macros(text)
882 macros = {}
883 macros = {}
883 text.gsub!(MACROS_RE) do
884 text.gsub!(MACROS_RE) do
884 all, macro = $1, $4.downcase
885 all, macro = $1, $4.downcase
885 if macro_exists?(macro) || all =~ MACRO_SUB_RE
886 if macro_exists?(macro) || all =~ MACRO_SUB_RE
886 index = macros.size
887 index = macros.size
887 macros[index] = all
888 macros[index] = all
888 "{{macro(#{index})}}"
889 "{{macro(#{index})}}"
889 else
890 else
890 all
891 all
891 end
892 end
892 end
893 end
893 macros
894 macros
894 end
895 end
895
896
896 # Executes and replaces macros in text
897 # Executes and replaces macros in text
897 def inject_macros(text, obj, macros, execute=true)
898 def inject_macros(text, obj, macros, execute=true)
898 text.gsub!(MACRO_SUB_RE) do
899 text.gsub!(MACRO_SUB_RE) do
899 all, index = $1, $2.to_i
900 all, index = $1, $2.to_i
900 orig = macros.delete(index)
901 orig = macros.delete(index)
901 if execute && orig && orig =~ MACROS_RE
902 if execute && orig && orig =~ MACROS_RE
902 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
903 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
903 if esc.nil?
904 if esc.nil?
904 h(exec_macro(macro, obj, args, block) || all)
905 h(exec_macro(macro, obj, args, block) || all)
905 else
906 else
906 h(all)
907 h(all)
907 end
908 end
908 elsif orig
909 elsif orig
909 h(orig)
910 h(orig)
910 else
911 else
911 h(all)
912 h(all)
912 end
913 end
913 end
914 end
914 end
915 end
915
916
916 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
917 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
917
918
918 # Renders the TOC with given headings
919 # Renders the TOC with given headings
919 def replace_toc(text, headings)
920 def replace_toc(text, headings)
920 text.gsub!(TOC_RE) do
921 text.gsub!(TOC_RE) do
921 # Keep only the 4 first levels
922 # Keep only the 4 first levels
922 headings = headings.select{|level, anchor, item| level <= 4}
923 headings = headings.select{|level, anchor, item| level <= 4}
923 if headings.empty?
924 if headings.empty?
924 ''
925 ''
925 else
926 else
926 div_class = 'toc'
927 div_class = 'toc'
927 div_class << ' right' if $1 == '>'
928 div_class << ' right' if $1 == '>'
928 div_class << ' left' if $1 == '<'
929 div_class << ' left' if $1 == '<'
929 out = "<ul class=\"#{div_class}\"><li>"
930 out = "<ul class=\"#{div_class}\"><li>"
930 root = headings.map(&:first).min
931 root = headings.map(&:first).min
931 current = root
932 current = root
932 started = false
933 started = false
933 headings.each do |level, anchor, item|
934 headings.each do |level, anchor, item|
934 if level > current
935 if level > current
935 out << '<ul><li>' * (level - current)
936 out << '<ul><li>' * (level - current)
936 elsif level < current
937 elsif level < current
937 out << "</li></ul>\n" * (current - level) + "</li><li>"
938 out << "</li></ul>\n" * (current - level) + "</li><li>"
938 elsif started
939 elsif started
939 out << '</li><li>'
940 out << '</li><li>'
940 end
941 end
941 out << "<a href=\"##{anchor}\">#{item}</a>"
942 out << "<a href=\"##{anchor}\">#{item}</a>"
942 current = level
943 current = level
943 started = true
944 started = true
944 end
945 end
945 out << '</li></ul>' * (current - root)
946 out << '</li></ul>' * (current - root)
946 out << '</li></ul>'
947 out << '</li></ul>'
947 end
948 end
948 end
949 end
949 end
950 end
950
951
951 # Same as Rails' simple_format helper without using paragraphs
952 # Same as Rails' simple_format helper without using paragraphs
952 def simple_format_without_paragraph(text)
953 def simple_format_without_paragraph(text)
953 text.to_s.
954 text.to_s.
954 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
955 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
955 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
956 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
956 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
957 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
957 html_safe
958 html_safe
958 end
959 end
959
960
960 def lang_options_for_select(blank=true)
961 def lang_options_for_select(blank=true)
961 (blank ? [["(auto)", ""]] : []) + languages_options
962 (blank ? [["(auto)", ""]] : []) + languages_options
962 end
963 end
963
964
964 def label_tag_for(name, option_tags = nil, options = {})
965 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"): "")
966 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)
967 content_tag("label", label_text)
967 end
968 end
968
969
969 def labelled_form_for(*args, &proc)
970 def labelled_form_for(*args, &proc)
970 args << {} unless args.last.is_a?(Hash)
971 args << {} unless args.last.is_a?(Hash)
971 options = args.last
972 options = args.last
972 if args.first.is_a?(Symbol)
973 if args.first.is_a?(Symbol)
973 options.merge!(:as => args.shift)
974 options.merge!(:as => args.shift)
974 end
975 end
975 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
976 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
976 form_for(*args, &proc)
977 form_for(*args, &proc)
977 end
978 end
978
979
979 def labelled_fields_for(*args, &proc)
980 def labelled_fields_for(*args, &proc)
980 args << {} unless args.last.is_a?(Hash)
981 args << {} unless args.last.is_a?(Hash)
981 options = args.last
982 options = args.last
982 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
983 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
983 fields_for(*args, &proc)
984 fields_for(*args, &proc)
984 end
985 end
985
986
986 def labelled_remote_form_for(*args, &proc)
987 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."
988 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)
989 args << {} unless args.last.is_a?(Hash)
989 options = args.last
990 options = args.last
990 options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
991 options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
991 form_for(*args, &proc)
992 form_for(*args, &proc)
992 end
993 end
993
994
994 def error_messages_for(*objects)
995 def error_messages_for(*objects)
995 html = ""
996 html = ""
996 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
997 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
997 errors = objects.map {|o| o.errors.full_messages}.flatten
998 errors = objects.map {|o| o.errors.full_messages}.flatten
998 if errors.any?
999 if errors.any?
999 html << "<div id='errorExplanation'><ul>\n"
1000 html << "<div id='errorExplanation'><ul>\n"
1000 errors.each do |error|
1001 errors.each do |error|
1001 html << "<li>#{h error}</li>\n"
1002 html << "<li>#{h error}</li>\n"
1002 end
1003 end
1003 html << "</ul></div>\n"
1004 html << "</ul></div>\n"
1004 end
1005 end
1005 html.html_safe
1006 html.html_safe
1006 end
1007 end
1007
1008
1008 def delete_link(url, options={})
1009 def delete_link(url, options={})
1009 options = {
1010 options = {
1010 :method => :delete,
1011 :method => :delete,
1011 :data => {:confirm => l(:text_are_you_sure)},
1012 :data => {:confirm => l(:text_are_you_sure)},
1012 :class => 'icon icon-del'
1013 :class => 'icon icon-del'
1013 }.merge(options)
1014 }.merge(options)
1014
1015
1015 link_to l(:button_delete), url, options
1016 link_to l(:button_delete), url, options
1016 end
1017 end
1017
1018
1018 def preview_link(url, form, target='preview', options={})
1019 def preview_link(url, form, target='preview', options={})
1019 content_tag 'a', l(:label_preview), {
1020 content_tag 'a', l(:label_preview), {
1020 :href => "#",
1021 :href => "#",
1021 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1022 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1022 :accesskey => accesskey(:preview)
1023 :accesskey => accesskey(:preview)
1023 }.merge(options)
1024 }.merge(options)
1024 end
1025 end
1025
1026
1026 def link_to_function(name, function, html_options={})
1027 def link_to_function(name, function, html_options={})
1027 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1028 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1028 end
1029 end
1029
1030
1030 # Helper to render JSON in views
1031 # Helper to render JSON in views
1031 def raw_json(arg)
1032 def raw_json(arg)
1032 arg.to_json.to_s.gsub('/', '\/').html_safe
1033 arg.to_json.to_s.gsub('/', '\/').html_safe
1033 end
1034 end
1034
1035
1035 def back_url
1036 def back_url
1036 url = params[:back_url]
1037 url = params[:back_url]
1037 if url.nil? && referer = request.env['HTTP_REFERER']
1038 if url.nil? && referer = request.env['HTTP_REFERER']
1038 url = CGI.unescape(referer.to_s)
1039 url = CGI.unescape(referer.to_s)
1039 end
1040 end
1040 url
1041 url
1041 end
1042 end
1042
1043
1043 def back_url_hidden_field_tag
1044 def back_url_hidden_field_tag
1044 url = back_url
1045 url = back_url
1045 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1046 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1046 end
1047 end
1047
1048
1048 def check_all_links(form_name)
1049 def check_all_links(form_name)
1049 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1050 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1050 " | ".html_safe +
1051 " | ".html_safe +
1051 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1052 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1052 end
1053 end
1053
1054
1054 def progress_bar(pcts, options={})
1055 def progress_bar(pcts, options={})
1055 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1056 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1056 pcts = pcts.collect(&:round)
1057 pcts = pcts.collect(&:round)
1057 pcts[1] = pcts[1] - pcts[0]
1058 pcts[1] = pcts[1] - pcts[0]
1058 pcts << (100 - pcts[1] - pcts[0])
1059 pcts << (100 - pcts[1] - pcts[0])
1059 width = options[:width] || '100px;'
1060 width = options[:width] || '100px;'
1060 legend = options[:legend] || ''
1061 legend = options[:legend] || ''
1061 content_tag('table',
1062 content_tag('table',
1062 content_tag('tr',
1063 content_tag('tr',
1063 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1064 (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) +
1065 (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)
1066 (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 +
1067 ), :class => "progress progress-#{pcts[0]}", :style => "width: #{width};").html_safe +
1067 content_tag('p', legend, :class => 'percent').html_safe
1068 content_tag('p', legend, :class => 'percent').html_safe
1068 end
1069 end
1069
1070
1070 def checked_image(checked=true)
1071 def checked_image(checked=true)
1071 if checked
1072 if checked
1072 image_tag 'toggle_check.png'
1073 image_tag 'toggle_check.png'
1073 end
1074 end
1074 end
1075 end
1075
1076
1076 def context_menu(url)
1077 def context_menu(url)
1077 unless @context_menu_included
1078 unless @context_menu_included
1078 content_for :header_tags do
1079 content_for :header_tags do
1079 javascript_include_tag('context_menu') +
1080 javascript_include_tag('context_menu') +
1080 stylesheet_link_tag('context_menu')
1081 stylesheet_link_tag('context_menu')
1081 end
1082 end
1082 if l(:direction) == 'rtl'
1083 if l(:direction) == 'rtl'
1083 content_for :header_tags do
1084 content_for :header_tags do
1084 stylesheet_link_tag('context_menu_rtl')
1085 stylesheet_link_tag('context_menu_rtl')
1085 end
1086 end
1086 end
1087 end
1087 @context_menu_included = true
1088 @context_menu_included = true
1088 end
1089 end
1089 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1090 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1090 end
1091 end
1091
1092
1092 def calendar_for(field_id)
1093 def calendar_for(field_id)
1093 include_calendar_headers_tags
1094 include_calendar_headers_tags
1094 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1095 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1095 end
1096 end
1096
1097
1097 def include_calendar_headers_tags
1098 def include_calendar_headers_tags
1098 unless @calendar_headers_tags_included
1099 unless @calendar_headers_tags_included
1099 tags = javascript_include_tag("datepicker")
1100 tags = javascript_include_tag("datepicker")
1100 @calendar_headers_tags_included = true
1101 @calendar_headers_tags_included = true
1101 content_for :header_tags do
1102 content_for :header_tags do
1102 start_of_week = Setting.start_of_week
1103 start_of_week = Setting.start_of_week
1103 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1104 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
1105 # Redmine uses 1..7 (monday..sunday) in settings and locales
1105 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1106 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1106 start_of_week = start_of_week.to_i % 7
1107 start_of_week = start_of_week.to_i % 7
1107 tags << javascript_tag(
1108 tags << javascript_tag(
1108 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1109 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1109 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1110 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1110 path_to_image('/images/calendar.png') +
1111 path_to_image('/images/calendar.png') +
1111 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1112 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1112 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1113 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1113 "beforeShow: beforeShowDatePicker};")
1114 "beforeShow: beforeShowDatePicker};")
1114 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1115 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1115 unless jquery_locale == 'en'
1116 unless jquery_locale == 'en'
1116 tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js")
1117 tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js")
1117 end
1118 end
1118 tags
1119 tags
1119 end
1120 end
1120 end
1121 end
1121 end
1122 end
1122
1123
1123 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1124 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1124 # Examples:
1125 # Examples:
1125 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1126 # 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
1127 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1127 #
1128 #
1128 def stylesheet_link_tag(*sources)
1129 def stylesheet_link_tag(*sources)
1129 options = sources.last.is_a?(Hash) ? sources.pop : {}
1130 options = sources.last.is_a?(Hash) ? sources.pop : {}
1130 plugin = options.delete(:plugin)
1131 plugin = options.delete(:plugin)
1131 sources = sources.map do |source|
1132 sources = sources.map do |source|
1132 if plugin
1133 if plugin
1133 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1134 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1134 elsif current_theme && current_theme.stylesheets.include?(source)
1135 elsif current_theme && current_theme.stylesheets.include?(source)
1135 current_theme.stylesheet_path(source)
1136 current_theme.stylesheet_path(source)
1136 else
1137 else
1137 source
1138 source
1138 end
1139 end
1139 end
1140 end
1140 super sources, options
1141 super sources, options
1141 end
1142 end
1142
1143
1143 # Overrides Rails' image_tag with themes and plugins support.
1144 # Overrides Rails' image_tag with themes and plugins support.
1144 # Examples:
1145 # Examples:
1145 # image_tag('image.png') # => picks image.png from the current theme or defaults
1146 # 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
1147 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1147 #
1148 #
1148 def image_tag(source, options={})
1149 def image_tag(source, options={})
1149 if plugin = options.delete(:plugin)
1150 if plugin = options.delete(:plugin)
1150 source = "/plugin_assets/#{plugin}/images/#{source}"
1151 source = "/plugin_assets/#{plugin}/images/#{source}"
1151 elsif current_theme && current_theme.images.include?(source)
1152 elsif current_theme && current_theme.images.include?(source)
1152 source = current_theme.image_path(source)
1153 source = current_theme.image_path(source)
1153 end
1154 end
1154 super source, options
1155 super source, options
1155 end
1156 end
1156
1157
1157 # Overrides Rails' javascript_include_tag with plugins support
1158 # Overrides Rails' javascript_include_tag with plugins support
1158 # Examples:
1159 # Examples:
1159 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1160 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1160 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1161 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1161 #
1162 #
1162 def javascript_include_tag(*sources)
1163 def javascript_include_tag(*sources)
1163 options = sources.last.is_a?(Hash) ? sources.pop : {}
1164 options = sources.last.is_a?(Hash) ? sources.pop : {}
1164 if plugin = options.delete(:plugin)
1165 if plugin = options.delete(:plugin)
1165 sources = sources.map do |source|
1166 sources = sources.map do |source|
1166 if plugin
1167 if plugin
1167 "/plugin_assets/#{plugin}/javascripts/#{source}"
1168 "/plugin_assets/#{plugin}/javascripts/#{source}"
1168 else
1169 else
1169 source
1170 source
1170 end
1171 end
1171 end
1172 end
1172 end
1173 end
1173 super sources, options
1174 super sources, options
1174 end
1175 end
1175
1176
1176 # TODO: remove this in 2.5.0
1177 # TODO: remove this in 2.5.0
1177 def has_content?(name)
1178 def has_content?(name)
1178 content_for?(name)
1179 content_for?(name)
1179 end
1180 end
1180
1181
1181 def sidebar_content?
1182 def sidebar_content?
1182 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1183 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1183 end
1184 end
1184
1185
1185 def view_layouts_base_sidebar_hook_response
1186 def view_layouts_base_sidebar_hook_response
1186 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1187 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1187 end
1188 end
1188
1189
1189 def email_delivery_enabled?
1190 def email_delivery_enabled?
1190 !!ActionMailer::Base.perform_deliveries
1191 !!ActionMailer::Base.perform_deliveries
1191 end
1192 end
1192
1193
1193 # Returns the avatar image tag for the given +user+ if avatars are enabled
1194 # Returns the avatar image tag for the given +user+ if avatars are enabled
1194 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1195 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1195 def avatar(user, options = { })
1196 def avatar(user, options = { })
1196 if Setting.gravatar_enabled?
1197 if Setting.gravatar_enabled?
1197 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1198 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1198 email = nil
1199 email = nil
1199 if user.respond_to?(:mail)
1200 if user.respond_to?(:mail)
1200 email = user.mail
1201 email = user.mail
1201 elsif user.to_s =~ %r{<(.+?)>}
1202 elsif user.to_s =~ %r{<(.+?)>}
1202 email = $1
1203 email = $1
1203 end
1204 end
1204 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1205 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1205 else
1206 else
1206 ''
1207 ''
1207 end
1208 end
1208 end
1209 end
1209
1210
1210 def sanitize_anchor_name(anchor)
1211 def sanitize_anchor_name(anchor)
1211 if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java'
1212 if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java'
1212 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1213 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1213 else
1214 else
1214 # TODO: remove when ruby1.8 is no longer supported
1215 # TODO: remove when ruby1.8 is no longer supported
1215 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1216 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1216 end
1217 end
1217 end
1218 end
1218
1219
1219 # Returns the javascript tags that are included in the html layout head
1220 # Returns the javascript tags that are included in the html layout head
1220 def javascript_heads
1221 def javascript_heads
1221 tags = javascript_include_tag('jquery-1.8.3-ui-1.9.2-ujs-2.0.3', 'application')
1222 tags = javascript_include_tag('jquery-1.8.3-ui-1.9.2-ujs-2.0.3', 'application')
1222 unless User.current.pref.warn_on_leaving_unsaved == '0'
1223 unless User.current.pref.warn_on_leaving_unsaved == '0'
1223 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1224 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1224 end
1225 end
1225 tags
1226 tags
1226 end
1227 end
1227
1228
1228 def favicon
1229 def favicon
1229 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1230 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1230 end
1231 end
1231
1232
1232 def robot_exclusion_tag
1233 def robot_exclusion_tag
1233 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1234 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1234 end
1235 end
1235
1236
1236 # Returns true if arg is expected in the API response
1237 # Returns true if arg is expected in the API response
1237 def include_in_api_response?(arg)
1238 def include_in_api_response?(arg)
1238 unless @included_in_api_response
1239 unless @included_in_api_response
1239 param = params[:include]
1240 param = params[:include]
1240 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1241 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1241 @included_in_api_response.collect!(&:strip)
1242 @included_in_api_response.collect!(&:strip)
1242 end
1243 end
1243 @included_in_api_response.include?(arg.to_s)
1244 @included_in_api_response.include?(arg.to_s)
1244 end
1245 end
1245
1246
1246 # Returns options or nil if nometa param or X-Redmine-Nometa header
1247 # Returns options or nil if nometa param or X-Redmine-Nometa header
1247 # was set in the request
1248 # was set in the request
1248 def api_meta(options)
1249 def api_meta(options)
1249 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1250 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1250 # compatibility mode for activeresource clients that raise
1251 # compatibility mode for activeresource clients that raise
1251 # an error when unserializing an array with attributes
1252 # an error when unserializing an array with attributes
1252 nil
1253 nil
1253 else
1254 else
1254 options
1255 options
1255 end
1256 end
1256 end
1257 end
1257
1258
1258 private
1259 private
1259
1260
1260 def wiki_helper
1261 def wiki_helper
1261 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1262 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1262 extend helper
1263 extend helper
1263 return self
1264 return self
1264 end
1265 end
1265
1266
1266 def link_to_content_update(text, url_params = {}, html_options = {})
1267 def link_to_content_update(text, url_params = {}, html_options = {})
1267 link_to(text, url_params, html_options)
1268 link_to(text, url_params, html_options)
1268 end
1269 end
1269 end
1270 end
@@ -1,428 +1,428
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 module IssuesHelper
20 module IssuesHelper
21 include ApplicationHelper
21 include ApplicationHelper
22
22
23 def issue_list(issues, &block)
23 def issue_list(issues, &block)
24 ancestors = []
24 ancestors = []
25 issues.each do |issue|
25 issues.each do |issue|
26 while (ancestors.any? && !issue.is_descendant_of?(ancestors.last))
26 while (ancestors.any? && !issue.is_descendant_of?(ancestors.last))
27 ancestors.pop
27 ancestors.pop
28 end
28 end
29 yield issue, ancestors.size
29 yield issue, ancestors.size
30 ancestors << issue unless issue.leaf?
30 ancestors << issue unless issue.leaf?
31 end
31 end
32 end
32 end
33
33
34 # Renders a HTML/CSS tooltip
34 # Renders a HTML/CSS tooltip
35 #
35 #
36 # To use, a trigger div is needed. This is a div with the class of "tooltip"
36 # To use, a trigger div is needed. This is a div with the class of "tooltip"
37 # that contains this method wrapped in a span with the class of "tip"
37 # that contains this method wrapped in a span with the class of "tip"
38 #
38 #
39 # <div class="tooltip"><%= link_to_issue(issue) %>
39 # <div class="tooltip"><%= link_to_issue(issue) %>
40 # <span class="tip"><%= render_issue_tooltip(issue) %></span>
40 # <span class="tip"><%= render_issue_tooltip(issue) %></span>
41 # </div>
41 # </div>
42 #
42 #
43 def render_issue_tooltip(issue)
43 def render_issue_tooltip(issue)
44 @cached_label_status ||= l(:field_status)
44 @cached_label_status ||= l(:field_status)
45 @cached_label_start_date ||= l(:field_start_date)
45 @cached_label_start_date ||= l(:field_start_date)
46 @cached_label_due_date ||= l(:field_due_date)
46 @cached_label_due_date ||= l(:field_due_date)
47 @cached_label_assigned_to ||= l(:field_assigned_to)
47 @cached_label_assigned_to ||= l(:field_assigned_to)
48 @cached_label_priority ||= l(:field_priority)
48 @cached_label_priority ||= l(:field_priority)
49 @cached_label_project ||= l(:field_project)
49 @cached_label_project ||= l(:field_project)
50
50
51 link_to_issue(issue) + "<br /><br />".html_safe +
51 link_to_issue(issue) + "<br /><br />".html_safe +
52 "<strong>#{@cached_label_project}</strong>: #{link_to_project(issue.project)}<br />".html_safe +
52 "<strong>#{@cached_label_project}</strong>: #{link_to_project(issue.project)}<br />".html_safe +
53 "<strong>#{@cached_label_status}</strong>: #{h(issue.status.name)}<br />".html_safe +
53 "<strong>#{@cached_label_status}</strong>: #{h(issue.status.name)}<br />".html_safe +
54 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />".html_safe +
54 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />".html_safe +
55 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />".html_safe +
55 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />".html_safe +
56 "<strong>#{@cached_label_assigned_to}</strong>: #{h(issue.assigned_to)}<br />".html_safe +
56 "<strong>#{@cached_label_assigned_to}</strong>: #{h(issue.assigned_to)}<br />".html_safe +
57 "<strong>#{@cached_label_priority}</strong>: #{h(issue.priority.name)}".html_safe
57 "<strong>#{@cached_label_priority}</strong>: #{h(issue.priority.name)}".html_safe
58 end
58 end
59
59
60 def issue_heading(issue)
60 def issue_heading(issue)
61 h("#{issue.tracker} ##{issue.id}")
61 h("#{issue.tracker} ##{issue.id}")
62 end
62 end
63
63
64 def render_issue_subject_with_tree(issue)
64 def render_issue_subject_with_tree(issue)
65 s = ''
65 s = ''
66 ancestors = issue.root? ? [] : issue.ancestors.visible.all
66 ancestors = issue.root? ? [] : issue.ancestors.visible.all
67 ancestors.each do |ancestor|
67 ancestors.each do |ancestor|
68 s << '<div>' + content_tag('p', link_to_issue(ancestor, :project => (issue.project_id != ancestor.project_id)))
68 s << '<div>' + content_tag('p', link_to_issue(ancestor, :project => (issue.project_id != ancestor.project_id)))
69 end
69 end
70 s << '<div>'
70 s << '<div>'
71 subject = h(issue.subject)
71 subject = h(issue.subject)
72 if issue.is_private?
72 if issue.is_private?
73 subject = content_tag('span', l(:field_is_private), :class => 'private') + ' ' + subject
73 subject = content_tag('span', l(:field_is_private), :class => 'private') + ' ' + subject
74 end
74 end
75 s << content_tag('h3', subject)
75 s << content_tag('h3', subject)
76 s << '</div>' * (ancestors.size + 1)
76 s << '</div>' * (ancestors.size + 1)
77 s.html_safe
77 s.html_safe
78 end
78 end
79
79
80 def render_descendants_tree(issue)
80 def render_descendants_tree(issue)
81 s = '<form><table class="list issues">'
81 s = '<form><table class="list issues">'
82 issue_list(issue.descendants.visible.sort_by(&:lft)) do |child, level|
82 issue_list(issue.descendants.visible.sort_by(&:lft)) do |child, level|
83 css = "issue issue-#{child.id} hascontextmenu"
83 css = "issue issue-#{child.id} hascontextmenu"
84 css << " idnt idnt-#{level}" if level > 0
84 css << " idnt idnt-#{level}" if level > 0
85 s << content_tag('tr',
85 s << content_tag('tr',
86 content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') +
86 content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') +
87 content_tag('td', link_to_issue(child, :truncate => 60, :project => (issue.project_id != child.project_id)), :class => 'subject') +
87 content_tag('td', link_to_issue(child, :truncate => 60, :project => (issue.project_id != child.project_id)), :class => 'subject') +
88 content_tag('td', h(child.status)) +
88 content_tag('td', h(child.status)) +
89 content_tag('td', link_to_user(child.assigned_to)) +
89 content_tag('td', link_to_user(child.assigned_to)) +
90 content_tag('td', progress_bar(child.done_ratio, :width => '80px')),
90 content_tag('td', progress_bar(child.done_ratio, :width => '80px')),
91 :class => css)
91 :class => css)
92 end
92 end
93 s << '</table></form>'
93 s << '</table></form>'
94 s.html_safe
94 s.html_safe
95 end
95 end
96
96
97 # Returns an array of error messages for bulk edited issues
97 # Returns an array of error messages for bulk edited issues
98 def bulk_edit_error_messages(issues)
98 def bulk_edit_error_messages(issues)
99 messages = {}
99 messages = {}
100 issues.each do |issue|
100 issues.each do |issue|
101 issue.errors.full_messages.each do |message|
101 issue.errors.full_messages.each do |message|
102 messages[message] ||= []
102 messages[message] ||= []
103 messages[message] << issue
103 messages[message] << issue
104 end
104 end
105 end
105 end
106 messages.map { |message, issues|
106 messages.map { |message, issues|
107 "#{message}: " + issues.map {|i| "##{i.id}"}.join(', ')
107 "#{message}: " + issues.map {|i| "##{i.id}"}.join(', ')
108 }
108 }
109 end
109 end
110
110
111 # Returns a link for adding a new subtask to the given issue
111 # Returns a link for adding a new subtask to the given issue
112 def link_to_new_subtask(issue)
112 def link_to_new_subtask(issue)
113 attrs = {
113 attrs = {
114 :tracker_id => issue.tracker,
114 :tracker_id => issue.tracker,
115 :parent_issue_id => issue
115 :parent_issue_id => issue
116 }
116 }
117 link_to(l(:button_add), new_project_issue_path(issue.project, :issue => attrs))
117 link_to(l(:button_add), new_project_issue_path(issue.project, :issue => attrs))
118 end
118 end
119
119
120 class IssueFieldsRows
120 class IssueFieldsRows
121 include ActionView::Helpers::TagHelper
121 include ActionView::Helpers::TagHelper
122
122
123 def initialize
123 def initialize
124 @left = []
124 @left = []
125 @right = []
125 @right = []
126 end
126 end
127
127
128 def left(*args)
128 def left(*args)
129 args.any? ? @left << cells(*args) : @left
129 args.any? ? @left << cells(*args) : @left
130 end
130 end
131
131
132 def right(*args)
132 def right(*args)
133 args.any? ? @right << cells(*args) : @right
133 args.any? ? @right << cells(*args) : @right
134 end
134 end
135
135
136 def size
136 def size
137 @left.size > @right.size ? @left.size : @right.size
137 @left.size > @right.size ? @left.size : @right.size
138 end
138 end
139
139
140 def to_html
140 def to_html
141 html = ''.html_safe
141 html = ''.html_safe
142 blank = content_tag('th', '') + content_tag('td', '')
142 blank = content_tag('th', '') + content_tag('td', '')
143 size.times do |i|
143 size.times do |i|
144 left = @left[i] || blank
144 left = @left[i] || blank
145 right = @right[i] || blank
145 right = @right[i] || blank
146 html << content_tag('tr', left + right)
146 html << content_tag('tr', left + right)
147 end
147 end
148 html
148 html
149 end
149 end
150
150
151 def cells(label, text, options={})
151 def cells(label, text, options={})
152 content_tag('th', "#{label}:", options) + content_tag('td', text, options)
152 content_tag('th', "#{label}:", options) + content_tag('td', text, options)
153 end
153 end
154 end
154 end
155
155
156 def issue_fields_rows
156 def issue_fields_rows
157 r = IssueFieldsRows.new
157 r = IssueFieldsRows.new
158 yield r
158 yield r
159 r.to_html
159 r.to_html
160 end
160 end
161
161
162 def render_custom_fields_rows(issue)
162 def render_custom_fields_rows(issue)
163 values = issue.visible_custom_field_values
163 values = issue.visible_custom_field_values
164 return if values.empty?
164 return if values.empty?
165 ordered_values = []
165 ordered_values = []
166 half = (values.size / 2.0).ceil
166 half = (values.size / 2.0).ceil
167 half.times do |i|
167 half.times do |i|
168 ordered_values << values[i]
168 ordered_values << values[i]
169 ordered_values << values[i + half]
169 ordered_values << values[i + half]
170 end
170 end
171 s = "<tr>\n"
171 s = "<tr>\n"
172 n = 0
172 n = 0
173 ordered_values.compact.each do |value|
173 ordered_values.compact.each do |value|
174 s << "</tr>\n<tr>\n" if n > 0 && (n % 2) == 0
174 s << "</tr>\n<tr>\n" if n > 0 && (n % 2) == 0
175 s << "\t<th>#{ h(value.custom_field.name) }:</th><td>#{ simple_format_without_paragraph(h(show_value(value))) }</td>\n"
175 s << "\t<th>#{ h(value.custom_field.name) }:</th><td>#{ simple_format_without_paragraph(h(show_value(value))) }</td>\n"
176 n += 1
176 n += 1
177 end
177 end
178 s << "</tr>\n"
178 s << "</tr>\n"
179 s.html_safe
179 s.html_safe
180 end
180 end
181
181
182 def issues_destroy_confirmation_message(issues)
182 def issues_destroy_confirmation_message(issues)
183 issues = [issues] unless issues.is_a?(Array)
183 issues = [issues] unless issues.is_a?(Array)
184 message = l(:text_issues_destroy_confirmation)
184 message = l(:text_issues_destroy_confirmation)
185 descendant_count = issues.inject(0) {|memo, i| memo += (i.right - i.left - 1)/2}
185 descendant_count = issues.inject(0) {|memo, i| memo += (i.right - i.left - 1)/2}
186 if descendant_count > 0
186 if descendant_count > 0
187 issues.each do |issue|
187 issues.each do |issue|
188 next if issue.root?
188 next if issue.root?
189 issues.each do |other_issue|
189 issues.each do |other_issue|
190 descendant_count -= 1 if issue.is_descendant_of?(other_issue)
190 descendant_count -= 1 if issue.is_descendant_of?(other_issue)
191 end
191 end
192 end
192 end
193 if descendant_count > 0
193 if descendant_count > 0
194 message << "\n" + l(:text_issues_destroy_descendants_confirmation, :count => descendant_count)
194 message << "\n" + l(:text_issues_destroy_descendants_confirmation, :count => descendant_count)
195 end
195 end
196 end
196 end
197 message
197 message
198 end
198 end
199
199
200 def sidebar_queries
200 def sidebar_queries
201 unless @sidebar_queries
201 unless @sidebar_queries
202 @sidebar_queries = IssueQuery.visible.
202 @sidebar_queries = IssueQuery.visible.
203 order("#{Query.table_name}.name ASC").
203 order("#{Query.table_name}.name ASC").
204 # Project specific queries and global queries
204 # Project specific queries and global queries
205 where(@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id]).
205 where(@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id]).
206 all
206 all
207 end
207 end
208 @sidebar_queries
208 @sidebar_queries
209 end
209 end
210
210
211 def query_links(title, queries)
211 def query_links(title, queries)
212 return '' if queries.empty?
212 return '' if queries.empty?
213 # links to #index on issues/show
213 # links to #index on issues/show
214 url_params = controller_name == 'issues' ? {:controller => 'issues', :action => 'index', :project_id => @project} : params
214 url_params = controller_name == 'issues' ? {:controller => 'issues', :action => 'index', :project_id => @project} : params
215
215
216 content_tag('h3', title) + "\n" +
216 content_tag('h3', title) + "\n" +
217 content_tag('ul',
217 content_tag('ul',
218 queries.collect {|query|
218 queries.collect {|query|
219 css = 'query'
219 css = 'query'
220 css << ' selected' if query == @query
220 css << ' selected' if query == @query
221 content_tag('li', link_to(query.name, url_params.merge(:query_id => query), :class => css))
221 content_tag('li', link_to(query.name, url_params.merge(:query_id => query), :class => css))
222 }.join("\n").html_safe,
222 }.join("\n").html_safe,
223 :class => 'queries'
223 :class => 'queries'
224 ) + "\n"
224 ) + "\n"
225 end
225 end
226
226
227 def render_sidebar_queries
227 def render_sidebar_queries
228 out = ''.html_safe
228 out = ''.html_safe
229 out << query_links(l(:label_my_queries), sidebar_queries.select(&:is_private?))
229 out << query_links(l(:label_my_queries), sidebar_queries.select(&:is_private?))
230 out << query_links(l(:label_query_plural), sidebar_queries.reject(&:is_private?))
230 out << query_links(l(:label_query_plural), sidebar_queries.reject(&:is_private?))
231 out
231 out
232 end
232 end
233
233
234 def email_issue_attributes(issue, user)
234 def email_issue_attributes(issue, user)
235 items = []
235 items = []
236 %w(author status priority assigned_to category fixed_version).each do |attribute|
236 %w(author status priority assigned_to category fixed_version).each do |attribute|
237 unless issue.disabled_core_fields.include?(attribute+"_id")
237 unless issue.disabled_core_fields.include?(attribute+"_id")
238 items << "#{l("field_#{attribute}")}: #{issue.send attribute}"
238 items << "#{l("field_#{attribute}")}: #{issue.send attribute}"
239 end
239 end
240 end
240 end
241 issue.visible_custom_field_values(user).each do |value|
241 issue.visible_custom_field_values(user).each do |value|
242 items << "#{value.custom_field.name}: #{show_value(value)}"
242 items << "#{value.custom_field.name}: #{show_value(value)}"
243 end
243 end
244 items
244 items
245 end
245 end
246
246
247 def render_email_issue_attributes(issue, user, html=false)
247 def render_email_issue_attributes(issue, user, html=false)
248 items = email_issue_attributes(issue, user)
248 items = email_issue_attributes(issue, user)
249 if html
249 if html
250 content_tag('ul', items.map{|s| content_tag('li', s)}.join("\n").html_safe)
250 content_tag('ul', items.map{|s| content_tag('li', s)}.join("\n").html_safe)
251 else
251 else
252 items.map{|s| "* #{s}"}.join("\n")
252 items.map{|s| "* #{s}"}.join("\n")
253 end
253 end
254 end
254 end
255
255
256 # Returns the textual representation of a journal details
256 # Returns the textual representation of a journal details
257 # as an array of strings
257 # as an array of strings
258 def details_to_strings(details, no_html=false, options={})
258 def details_to_strings(details, no_html=false, options={})
259 options[:only_path] = (options[:only_path] == false ? false : true)
259 options[:only_path] = (options[:only_path] == false ? false : true)
260 strings = []
260 strings = []
261 values_by_field = {}
261 values_by_field = {}
262 details.each do |detail|
262 details.each do |detail|
263 if detail.property == 'cf'
263 if detail.property == 'cf'
264 field = detail.custom_field
264 field = detail.custom_field
265 if field && field.multiple?
265 if field && field.multiple?
266 values_by_field[field] ||= {:added => [], :deleted => []}
266 values_by_field[field] ||= {:added => [], :deleted => []}
267 if detail.old_value
267 if detail.old_value
268 values_by_field[field][:deleted] << detail.old_value
268 values_by_field[field][:deleted] << detail.old_value
269 end
269 end
270 if detail.value
270 if detail.value
271 values_by_field[field][:added] << detail.value
271 values_by_field[field][:added] << detail.value
272 end
272 end
273 next
273 next
274 end
274 end
275 end
275 end
276 strings << show_detail(detail, no_html, options)
276 strings << show_detail(detail, no_html, options)
277 end
277 end
278 values_by_field.each do |field, changes|
278 values_by_field.each do |field, changes|
279 detail = JournalDetail.new(:property => 'cf', :prop_key => field.id.to_s)
279 detail = JournalDetail.new(:property => 'cf', :prop_key => field.id.to_s)
280 detail.instance_variable_set "@custom_field", field
280 detail.instance_variable_set "@custom_field", field
281 if changes[:added].any?
281 if changes[:added].any?
282 detail.value = changes[:added]
282 detail.value = changes[:added]
283 strings << show_detail(detail, no_html, options)
283 strings << show_detail(detail, no_html, options)
284 elsif changes[:deleted].any?
284 elsif changes[:deleted].any?
285 detail.old_value = changes[:deleted]
285 detail.old_value = changes[:deleted]
286 strings << show_detail(detail, no_html, options)
286 strings << show_detail(detail, no_html, options)
287 end
287 end
288 end
288 end
289 strings
289 strings
290 end
290 end
291
291
292 # Returns the textual representation of a single journal detail
292 # Returns the textual representation of a single journal detail
293 def show_detail(detail, no_html=false, options={})
293 def show_detail(detail, no_html=false, options={})
294 multiple = false
294 multiple = false
295 case detail.property
295 case detail.property
296 when 'attr'
296 when 'attr'
297 field = detail.prop_key.to_s.gsub(/\_id$/, "")
297 field = detail.prop_key.to_s.gsub(/\_id$/, "")
298 label = l(("field_" + field).to_sym)
298 label = l(("field_" + field).to_sym)
299 case detail.prop_key
299 case detail.prop_key
300 when 'due_date', 'start_date'
300 when 'due_date', 'start_date'
301 value = format_date(detail.value.to_date) if detail.value
301 value = format_date(detail.value.to_date) if detail.value
302 old_value = format_date(detail.old_value.to_date) if detail.old_value
302 old_value = format_date(detail.old_value.to_date) if detail.old_value
303
303
304 when 'project_id', 'status_id', 'tracker_id', 'assigned_to_id',
304 when 'project_id', 'status_id', 'tracker_id', 'assigned_to_id',
305 'priority_id', 'category_id', 'fixed_version_id'
305 'priority_id', 'category_id', 'fixed_version_id'
306 value = find_name_by_reflection(field, detail.value)
306 value = find_name_by_reflection(field, detail.value)
307 old_value = find_name_by_reflection(field, detail.old_value)
307 old_value = find_name_by_reflection(field, detail.old_value)
308
308
309 when 'estimated_hours'
309 when 'estimated_hours'
310 value = "%0.02f" % detail.value.to_f unless detail.value.blank?
310 value = "%0.02f" % detail.value.to_f unless detail.value.blank?
311 old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank?
311 old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank?
312
312
313 when 'parent_id'
313 when 'parent_id'
314 label = l(:field_parent_issue)
314 label = l(:field_parent_issue)
315 value = "##{detail.value}" unless detail.value.blank?
315 value = "##{detail.value}" unless detail.value.blank?
316 old_value = "##{detail.old_value}" unless detail.old_value.blank?
316 old_value = "##{detail.old_value}" unless detail.old_value.blank?
317
317
318 when 'is_private'
318 when 'is_private'
319 value = l(detail.value == "0" ? :general_text_No : :general_text_Yes) unless detail.value.blank?
319 value = l(detail.value == "0" ? :general_text_No : :general_text_Yes) unless detail.value.blank?
320 old_value = l(detail.old_value == "0" ? :general_text_No : :general_text_Yes) unless detail.old_value.blank?
320 old_value = l(detail.old_value == "0" ? :general_text_No : :general_text_Yes) unless detail.old_value.blank?
321 end
321 end
322 when 'cf'
322 when 'cf'
323 custom_field = detail.custom_field
323 custom_field = detail.custom_field
324 if custom_field
324 if custom_field
325 multiple = custom_field.multiple?
325 multiple = custom_field.multiple?
326 label = custom_field.name
326 label = custom_field.name
327 value = format_value(detail.value, custom_field.field_format) if detail.value
327 value = format_value(detail.value, custom_field.field_format) if detail.value
328 old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value
328 old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value
329 end
329 end
330 when 'attachment'
330 when 'attachment'
331 label = l(:label_attachment)
331 label = l(:label_attachment)
332 when 'relation'
332 when 'relation'
333 if detail.value && !detail.old_value
333 if detail.value && !detail.old_value
334 rel_issue = Issue.visible.find_by_id(detail.value)
334 rel_issue = Issue.visible.find_by_id(detail.value)
335 value = rel_issue.nil? ? "#{l(:label_issue)} ##{detail.value}" :
335 value = rel_issue.nil? ? "#{l(:label_issue)} ##{detail.value}" :
336 (no_html ? rel_issue : link_to_issue(rel_issue))
336 (no_html ? rel_issue : link_to_issue(rel_issue, :only_path => options[:only_path]))
337 elsif detail.old_value && !detail.value
337 elsif detail.old_value && !detail.value
338 rel_issue = Issue.visible.find_by_id(detail.old_value)
338 rel_issue = Issue.visible.find_by_id(detail.old_value)
339 old_value = rel_issue.nil? ? "#{l(:label_issue)} ##{detail.old_value}" :
339 old_value = rel_issue.nil? ? "#{l(:label_issue)} ##{detail.old_value}" :
340 (no_html ? rel_issue : link_to_issue(rel_issue))
340 (no_html ? rel_issue : link_to_issue(rel_issue, :only_path => options[:only_path]))
341 end
341 end
342 label = l(detail.prop_key.to_sym)
342 label = l(detail.prop_key.to_sym)
343 end
343 end
344 call_hook(:helper_issues_show_detail_after_setting,
344 call_hook(:helper_issues_show_detail_after_setting,
345 {:detail => detail, :label => label, :value => value, :old_value => old_value })
345 {:detail => detail, :label => label, :value => value, :old_value => old_value })
346
346
347 label ||= detail.prop_key
347 label ||= detail.prop_key
348 value ||= detail.value
348 value ||= detail.value
349 old_value ||= detail.old_value
349 old_value ||= detail.old_value
350
350
351 unless no_html
351 unless no_html
352 label = content_tag('strong', label)
352 label = content_tag('strong', label)
353 old_value = content_tag("i", h(old_value)) if detail.old_value
353 old_value = content_tag("i", h(old_value)) if detail.old_value
354 if detail.old_value && detail.value.blank? && detail.property != 'relation'
354 if detail.old_value && detail.value.blank? && detail.property != 'relation'
355 old_value = content_tag("del", old_value)
355 old_value = content_tag("del", old_value)
356 end
356 end
357 if detail.property == 'attachment' && !value.blank? && atta = Attachment.find_by_id(detail.prop_key)
357 if detail.property == 'attachment' && !value.blank? && atta = Attachment.find_by_id(detail.prop_key)
358 # Link to the attachment if it has not been removed
358 # Link to the attachment if it has not been removed
359 value = link_to_attachment(atta, :download => true, :only_path => options[:only_path])
359 value = link_to_attachment(atta, :download => true, :only_path => options[:only_path])
360 if options[:only_path] != false && atta.is_text?
360 if options[:only_path] != false && atta.is_text?
361 value += link_to(
361 value += link_to(
362 image_tag('magnifier.png'),
362 image_tag('magnifier.png'),
363 :controller => 'attachments', :action => 'show',
363 :controller => 'attachments', :action => 'show',
364 :id => atta, :filename => atta.filename
364 :id => atta, :filename => atta.filename
365 )
365 )
366 end
366 end
367 else
367 else
368 value = content_tag("i", h(value)) if value
368 value = content_tag("i", h(value)) if value
369 end
369 end
370 end
370 end
371
371
372 if detail.property == 'attr' && detail.prop_key == 'description'
372 if detail.property == 'attr' && detail.prop_key == 'description'
373 s = l(:text_journal_changed_no_detail, :label => label)
373 s = l(:text_journal_changed_no_detail, :label => label)
374 unless no_html
374 unless no_html
375 diff_link = link_to 'diff',
375 diff_link = link_to 'diff',
376 {:controller => 'journals', :action => 'diff', :id => detail.journal_id,
376 {:controller => 'journals', :action => 'diff', :id => detail.journal_id,
377 :detail_id => detail.id, :only_path => options[:only_path]},
377 :detail_id => detail.id, :only_path => options[:only_path]},
378 :title => l(:label_view_diff)
378 :title => l(:label_view_diff)
379 s << " (#{ diff_link })"
379 s << " (#{ diff_link })"
380 end
380 end
381 s.html_safe
381 s.html_safe
382 elsif detail.value.present?
382 elsif detail.value.present?
383 case detail.property
383 case detail.property
384 when 'attr', 'cf'
384 when 'attr', 'cf'
385 if detail.old_value.present?
385 if detail.old_value.present?
386 l(:text_journal_changed, :label => label, :old => old_value, :new => value).html_safe
386 l(:text_journal_changed, :label => label, :old => old_value, :new => value).html_safe
387 elsif multiple
387 elsif multiple
388 l(:text_journal_added, :label => label, :value => value).html_safe
388 l(:text_journal_added, :label => label, :value => value).html_safe
389 else
389 else
390 l(:text_journal_set_to, :label => label, :value => value).html_safe
390 l(:text_journal_set_to, :label => label, :value => value).html_safe
391 end
391 end
392 when 'attachment', 'relation'
392 when 'attachment', 'relation'
393 l(:text_journal_added, :label => label, :value => value).html_safe
393 l(:text_journal_added, :label => label, :value => value).html_safe
394 end
394 end
395 else
395 else
396 l(:text_journal_deleted, :label => label, :old => old_value).html_safe
396 l(:text_journal_deleted, :label => label, :old => old_value).html_safe
397 end
397 end
398 end
398 end
399
399
400 # Find the name of an associated record stored in the field attribute
400 # Find the name of an associated record stored in the field attribute
401 def find_name_by_reflection(field, id)
401 def find_name_by_reflection(field, id)
402 unless id.present?
402 unless id.present?
403 return nil
403 return nil
404 end
404 end
405 association = Issue.reflect_on_association(field.to_sym)
405 association = Issue.reflect_on_association(field.to_sym)
406 if association
406 if association
407 record = association.class_name.constantize.find_by_id(id)
407 record = association.class_name.constantize.find_by_id(id)
408 if record
408 if record
409 record.name.force_encoding('UTF-8') if record.name.respond_to?(:force_encoding)
409 record.name.force_encoding('UTF-8') if record.name.respond_to?(:force_encoding)
410 return record.name
410 return record.name
411 end
411 end
412 end
412 end
413 end
413 end
414
414
415 # Renders issue children recursively
415 # Renders issue children recursively
416 def render_api_issue_children(issue, api)
416 def render_api_issue_children(issue, api)
417 return if issue.leaf?
417 return if issue.leaf?
418 api.array :children do
418 api.array :children do
419 issue.children.each do |child|
419 issue.children.each do |child|
420 api.issue(:id => child.id) do
420 api.issue(:id => child.id) do
421 api.tracker(:id => child.tracker_id, :name => child.tracker.name) unless child.tracker.nil?
421 api.tracker(:id => child.tracker_id, :name => child.tracker.name) unless child.tracker.nil?
422 api.subject child.subject
422 api.subject child.subject
423 render_api_issue_children(child, api)
423 render_api_issue_children(child, api)
424 end
424 end
425 end
425 end
426 end
426 end
427 end
427 end
428 end
428 end
@@ -1,747 +1,757
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 MailerTest < ActiveSupport::TestCase
20 class MailerTest < ActiveSupport::TestCase
21 include Redmine::I18n
21 include Redmine::I18n
22 include ActionDispatch::Assertions::SelectorAssertions
22 include ActionDispatch::Assertions::SelectorAssertions
23 fixtures :projects, :enabled_modules, :issues, :users, :members,
23 fixtures :projects, :enabled_modules, :issues, :users, :members,
24 :member_roles, :roles, :documents, :attachments, :news,
24 :member_roles, :roles, :documents, :attachments, :news,
25 :tokens, :journals, :journal_details, :changesets,
25 :tokens, :journals, :journal_details, :changesets,
26 :trackers, :projects_trackers,
26 :trackers, :projects_trackers,
27 :issue_statuses, :enumerations, :messages, :boards, :repositories,
27 :issue_statuses, :enumerations, :messages, :boards, :repositories,
28 :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions,
28 :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions,
29 :versions,
29 :versions,
30 :comments
30 :comments
31
31
32 def setup
32 def setup
33 ActionMailer::Base.deliveries.clear
33 ActionMailer::Base.deliveries.clear
34 Setting.host_name = 'mydomain.foo'
34 Setting.host_name = 'mydomain.foo'
35 Setting.protocol = 'http'
35 Setting.protocol = 'http'
36 Setting.plain_text_mail = '0'
36 Setting.plain_text_mail = '0'
37 end
37 end
38
38
39 def test_generated_links_in_emails
39 def test_generated_links_in_emails
40 Setting.default_language = 'en'
40 Setting.default_language = 'en'
41 Setting.host_name = 'mydomain.foo'
41 Setting.host_name = 'mydomain.foo'
42 Setting.protocol = 'https'
42 Setting.protocol = 'https'
43
43
44 journal = Journal.find(3)
44 journal = Journal.find(3)
45 assert Mailer.deliver_issue_edit(journal)
45 assert Mailer.deliver_issue_edit(journal)
46
46
47 mail = last_email
47 mail = last_email
48 assert_not_nil mail
48 assert_not_nil mail
49
49
50 assert_select_email do
50 assert_select_email do
51 # link to the main ticket
51 # link to the main ticket
52 assert_select 'a[href=?]',
52 assert_select 'a[href=?]',
53 'https://mydomain.foo/issues/2#change-3',
53 'https://mydomain.foo/issues/2#change-3',
54 :text => 'Feature request #2: Add ingredients categories'
54 :text => 'Feature request #2: Add ingredients categories'
55 # link to a referenced ticket
55 # link to a referenced ticket
56 assert_select 'a[href=?][title=?]',
56 assert_select 'a[href=?][title=?]',
57 'https://mydomain.foo/issues/1',
57 'https://mydomain.foo/issues/1',
58 'Can&#x27;t print recipes (New)',
58 'Can&#x27;t print recipes (New)',
59 :text => '#1'
59 :text => '#1'
60 # link to a changeset
60 # link to a changeset
61 assert_select 'a[href=?][title=?]',
61 assert_select 'a[href=?][title=?]',
62 'https://mydomain.foo/projects/ecookbook/repository/revisions/2',
62 'https://mydomain.foo/projects/ecookbook/repository/revisions/2',
63 'This commit fixes #1, #2 and references #1 &amp; #3',
63 'This commit fixes #1, #2 and references #1 &amp; #3',
64 :text => 'r2'
64 :text => 'r2'
65 # link to a description diff
65 # link to a description diff
66 assert_select 'a[href=?][title=?]',
66 assert_select 'a[href=?][title=?]',
67 'https://mydomain.foo/journals/diff/3?detail_id=4',
67 'https://mydomain.foo/journals/diff/3?detail_id=4',
68 'View differences',
68 'View differences',
69 :text => 'diff'
69 :text => 'diff'
70 # link to an attachment
70 # link to an attachment
71 assert_select 'a[href=?]',
71 assert_select 'a[href=?]',
72 'https://mydomain.foo/attachments/download/4/source.rb',
72 'https://mydomain.foo/attachments/download/4/source.rb',
73 :text => 'source.rb'
73 :text => 'source.rb'
74 end
74 end
75 end
75 end
76
76
77 def test_generated_links_with_prefix
77 def test_generated_links_with_prefix
78 Setting.default_language = 'en'
78 Setting.default_language = 'en'
79 relative_url_root = Redmine::Utils.relative_url_root
79 relative_url_root = Redmine::Utils.relative_url_root
80 Setting.host_name = 'mydomain.foo/rdm'
80 Setting.host_name = 'mydomain.foo/rdm'
81 Setting.protocol = 'http'
81 Setting.protocol = 'http'
82
82
83 journal = Journal.find(3)
83 journal = Journal.find(3)
84 assert Mailer.deliver_issue_edit(journal)
84 assert Mailer.deliver_issue_edit(journal)
85
85
86 mail = last_email
86 mail = last_email
87 assert_not_nil mail
87 assert_not_nil mail
88
88
89 assert_select_email do
89 assert_select_email do
90 # link to the main ticket
90 # link to the main ticket
91 assert_select 'a[href=?]',
91 assert_select 'a[href=?]',
92 'http://mydomain.foo/rdm/issues/2#change-3',
92 'http://mydomain.foo/rdm/issues/2#change-3',
93 :text => 'Feature request #2: Add ingredients categories'
93 :text => 'Feature request #2: Add ingredients categories'
94 # link to a referenced ticket
94 # link to a referenced ticket
95 assert_select 'a[href=?][title=?]',
95 assert_select 'a[href=?][title=?]',
96 'http://mydomain.foo/rdm/issues/1',
96 'http://mydomain.foo/rdm/issues/1',
97 'Can&#x27;t print recipes (New)',
97 'Can&#x27;t print recipes (New)',
98 :text => '#1'
98 :text => '#1'
99 # link to a changeset
99 # link to a changeset
100 assert_select 'a[href=?][title=?]',
100 assert_select 'a[href=?][title=?]',
101 'http://mydomain.foo/rdm/projects/ecookbook/repository/revisions/2',
101 'http://mydomain.foo/rdm/projects/ecookbook/repository/revisions/2',
102 'This commit fixes #1, #2 and references #1 &amp; #3',
102 'This commit fixes #1, #2 and references #1 &amp; #3',
103 :text => 'r2'
103 :text => 'r2'
104 # link to a description diff
104 # link to a description diff
105 assert_select 'a[href=?][title=?]',
105 assert_select 'a[href=?][title=?]',
106 'http://mydomain.foo/rdm/journals/diff/3?detail_id=4',
106 'http://mydomain.foo/rdm/journals/diff/3?detail_id=4',
107 'View differences',
107 'View differences',
108 :text => 'diff'
108 :text => 'diff'
109 # link to an attachment
109 # link to an attachment
110 assert_select 'a[href=?]',
110 assert_select 'a[href=?]',
111 'http://mydomain.foo/rdm/attachments/download/4/source.rb',
111 'http://mydomain.foo/rdm/attachments/download/4/source.rb',
112 :text => 'source.rb'
112 :text => 'source.rb'
113 end
113 end
114 end
114 end
115
115
116 def test_issue_edit_should_generate_url_with_hostname_for_relations
117 journal = Journal.new(:journalized => Issue.find(1), :user => User.find(1), :created_on => Time.now)
118 journal.details << JournalDetail.new(:property => 'relation', :prop_key => 'label_relates_to', :value => 2)
119 Mailer.deliver_issue_edit(journal)
120 assert_not_nil last_email
121 assert_select_email do
122 assert_select 'a[href=?]', 'http://mydomain.foo/issues/2', :text => 'Feature request #2'
123 end
124 end
125
116 def test_generated_links_with_prefix_and_no_relative_url_root
126 def test_generated_links_with_prefix_and_no_relative_url_root
117 Setting.default_language = 'en'
127 Setting.default_language = 'en'
118 relative_url_root = Redmine::Utils.relative_url_root
128 relative_url_root = Redmine::Utils.relative_url_root
119 Setting.host_name = 'mydomain.foo/rdm'
129 Setting.host_name = 'mydomain.foo/rdm'
120 Setting.protocol = 'http'
130 Setting.protocol = 'http'
121 Redmine::Utils.relative_url_root = nil
131 Redmine::Utils.relative_url_root = nil
122
132
123 journal = Journal.find(3)
133 journal = Journal.find(3)
124 assert Mailer.deliver_issue_edit(journal)
134 assert Mailer.deliver_issue_edit(journal)
125
135
126 mail = last_email
136 mail = last_email
127 assert_not_nil mail
137 assert_not_nil mail
128
138
129 assert_select_email do
139 assert_select_email do
130 # link to the main ticket
140 # link to the main ticket
131 assert_select 'a[href=?]',
141 assert_select 'a[href=?]',
132 'http://mydomain.foo/rdm/issues/2#change-3',
142 'http://mydomain.foo/rdm/issues/2#change-3',
133 :text => 'Feature request #2: Add ingredients categories'
143 :text => 'Feature request #2: Add ingredients categories'
134 # link to a referenced ticket
144 # link to a referenced ticket
135 assert_select 'a[href=?][title=?]',
145 assert_select 'a[href=?][title=?]',
136 'http://mydomain.foo/rdm/issues/1',
146 'http://mydomain.foo/rdm/issues/1',
137 'Can&#x27;t print recipes (New)',
147 'Can&#x27;t print recipes (New)',
138 :text => '#1'
148 :text => '#1'
139 # link to a changeset
149 # link to a changeset
140 assert_select 'a[href=?][title=?]',
150 assert_select 'a[href=?][title=?]',
141 'http://mydomain.foo/rdm/projects/ecookbook/repository/revisions/2',
151 'http://mydomain.foo/rdm/projects/ecookbook/repository/revisions/2',
142 'This commit fixes #1, #2 and references #1 &amp; #3',
152 'This commit fixes #1, #2 and references #1 &amp; #3',
143 :text => 'r2'
153 :text => 'r2'
144 # link to a description diff
154 # link to a description diff
145 assert_select 'a[href=?][title=?]',
155 assert_select 'a[href=?][title=?]',
146 'http://mydomain.foo/rdm/journals/diff/3?detail_id=4',
156 'http://mydomain.foo/rdm/journals/diff/3?detail_id=4',
147 'View differences',
157 'View differences',
148 :text => 'diff'
158 :text => 'diff'
149 # link to an attachment
159 # link to an attachment
150 assert_select 'a[href=?]',
160 assert_select 'a[href=?]',
151 'http://mydomain.foo/rdm/attachments/download/4/source.rb',
161 'http://mydomain.foo/rdm/attachments/download/4/source.rb',
152 :text => 'source.rb'
162 :text => 'source.rb'
153 end
163 end
154 ensure
164 ensure
155 # restore it
165 # restore it
156 Redmine::Utils.relative_url_root = relative_url_root
166 Redmine::Utils.relative_url_root = relative_url_root
157 end
167 end
158
168
159 def test_email_headers
169 def test_email_headers
160 issue = Issue.find(1)
170 issue = Issue.find(1)
161 Mailer.deliver_issue_add(issue)
171 Mailer.deliver_issue_add(issue)
162 mail = last_email
172 mail = last_email
163 assert_not_nil mail
173 assert_not_nil mail
164 assert_equal 'OOF', mail.header['X-Auto-Response-Suppress'].to_s
174 assert_equal 'OOF', mail.header['X-Auto-Response-Suppress'].to_s
165 assert_equal 'auto-generated', mail.header['Auto-Submitted'].to_s
175 assert_equal 'auto-generated', mail.header['Auto-Submitted'].to_s
166 assert_equal '<redmine.example.net>', mail.header['List-Id'].to_s
176 assert_equal '<redmine.example.net>', mail.header['List-Id'].to_s
167 end
177 end
168
178
169 def test_email_headers_should_include_sender
179 def test_email_headers_should_include_sender
170 issue = Issue.find(1)
180 issue = Issue.find(1)
171 Mailer.deliver_issue_add(issue)
181 Mailer.deliver_issue_add(issue)
172 mail = last_email
182 mail = last_email
173 assert_equal issue.author.login, mail.header['X-Redmine-Sender'].to_s
183 assert_equal issue.author.login, mail.header['X-Redmine-Sender'].to_s
174 end
184 end
175
185
176 def test_plain_text_mail
186 def test_plain_text_mail
177 Setting.plain_text_mail = 1
187 Setting.plain_text_mail = 1
178 journal = Journal.find(2)
188 journal = Journal.find(2)
179 Mailer.deliver_issue_edit(journal)
189 Mailer.deliver_issue_edit(journal)
180 mail = last_email
190 mail = last_email
181 assert_equal "text/plain; charset=UTF-8", mail.content_type
191 assert_equal "text/plain; charset=UTF-8", mail.content_type
182 assert_equal 0, mail.parts.size
192 assert_equal 0, mail.parts.size
183 assert !mail.encoded.include?('href')
193 assert !mail.encoded.include?('href')
184 end
194 end
185
195
186 def test_html_mail
196 def test_html_mail
187 Setting.plain_text_mail = 0
197 Setting.plain_text_mail = 0
188 journal = Journal.find(2)
198 journal = Journal.find(2)
189 Mailer.deliver_issue_edit(journal)
199 Mailer.deliver_issue_edit(journal)
190 mail = last_email
200 mail = last_email
191 assert_equal 2, mail.parts.size
201 assert_equal 2, mail.parts.size
192 assert mail.encoded.include?('href')
202 assert mail.encoded.include?('href')
193 end
203 end
194
204
195 def test_from_header
205 def test_from_header
196 with_settings :mail_from => 'redmine@example.net' do
206 with_settings :mail_from => 'redmine@example.net' do
197 Mailer.test_email(User.find(1)).deliver
207 Mailer.test_email(User.find(1)).deliver
198 end
208 end
199 mail = last_email
209 mail = last_email
200 assert_equal 'redmine@example.net', mail.from_addrs.first
210 assert_equal 'redmine@example.net', mail.from_addrs.first
201 end
211 end
202
212
203 def test_from_header_with_phrase
213 def test_from_header_with_phrase
204 with_settings :mail_from => 'Redmine app <redmine@example.net>' do
214 with_settings :mail_from => 'Redmine app <redmine@example.net>' do
205 Mailer.test_email(User.find(1)).deliver
215 Mailer.test_email(User.find(1)).deliver
206 end
216 end
207 mail = last_email
217 mail = last_email
208 assert_equal 'redmine@example.net', mail.from_addrs.first
218 assert_equal 'redmine@example.net', mail.from_addrs.first
209 assert_equal 'Redmine app <redmine@example.net>', mail.header['From'].to_s
219 assert_equal 'Redmine app <redmine@example.net>', mail.header['From'].to_s
210 end
220 end
211
221
212 def test_should_not_send_email_without_recipient
222 def test_should_not_send_email_without_recipient
213 news = News.first
223 news = News.first
214 user = news.author
224 user = news.author
215 # Remove members except news author
225 # Remove members except news author
216 news.project.memberships.each {|m| m.destroy unless m.user == user}
226 news.project.memberships.each {|m| m.destroy unless m.user == user}
217
227
218 user.pref.no_self_notified = false
228 user.pref.no_self_notified = false
219 user.pref.save
229 user.pref.save
220 User.current = user
230 User.current = user
221 Mailer.news_added(news.reload).deliver
231 Mailer.news_added(news.reload).deliver
222 assert_equal 1, last_email.bcc.size
232 assert_equal 1, last_email.bcc.size
223
233
224 # nobody to notify
234 # nobody to notify
225 user.pref.no_self_notified = true
235 user.pref.no_self_notified = true
226 user.pref.save
236 user.pref.save
227 User.current = user
237 User.current = user
228 ActionMailer::Base.deliveries.clear
238 ActionMailer::Base.deliveries.clear
229 Mailer.news_added(news.reload).deliver
239 Mailer.news_added(news.reload).deliver
230 assert ActionMailer::Base.deliveries.empty?
240 assert ActionMailer::Base.deliveries.empty?
231 end
241 end
232
242
233 def test_issue_add_message_id
243 def test_issue_add_message_id
234 issue = Issue.find(2)
244 issue = Issue.find(2)
235 Mailer.deliver_issue_add(issue)
245 Mailer.deliver_issue_add(issue)
236 mail = last_email
246 mail = last_email
237 assert_match /^redmine\.issue-2\.20060719190421\.[a-f0-9]+@example\.net/, mail.message_id
247 assert_match /^redmine\.issue-2\.20060719190421\.[a-f0-9]+@example\.net/, mail.message_id
238 assert_include "redmine.issue-2.20060719190421@example.net", mail.references
248 assert_include "redmine.issue-2.20060719190421@example.net", mail.references
239 end
249 end
240
250
241 def test_issue_edit_message_id
251 def test_issue_edit_message_id
242 journal = Journal.find(3)
252 journal = Journal.find(3)
243 journal.issue = Issue.find(2)
253 journal.issue = Issue.find(2)
244
254
245 Mailer.deliver_issue_edit(journal)
255 Mailer.deliver_issue_edit(journal)
246 mail = last_email
256 mail = last_email
247 assert_match /^redmine\.journal-3\.\d+\.[a-f0-9]+@example\.net/, mail.message_id
257 assert_match /^redmine\.journal-3\.\d+\.[a-f0-9]+@example\.net/, mail.message_id
248 assert_include "redmine.issue-2.20060719190421@example.net", mail.references
258 assert_include "redmine.issue-2.20060719190421@example.net", mail.references
249 assert_select_email do
259 assert_select_email do
250 # link to the update
260 # link to the update
251 assert_select "a[href=?]",
261 assert_select "a[href=?]",
252 "http://mydomain.foo/issues/#{journal.journalized_id}#change-#{journal.id}"
262 "http://mydomain.foo/issues/#{journal.journalized_id}#change-#{journal.id}"
253 end
263 end
254 end
264 end
255
265
256 def test_message_posted_message_id
266 def test_message_posted_message_id
257 message = Message.find(1)
267 message = Message.find(1)
258 Mailer.message_posted(message).deliver
268 Mailer.message_posted(message).deliver
259 mail = last_email
269 mail = last_email
260 assert_match /^redmine\.message-1\.\d+\.[a-f0-9]+@example\.net/, mail.message_id
270 assert_match /^redmine\.message-1\.\d+\.[a-f0-9]+@example\.net/, mail.message_id
261 assert_include "redmine.message-1.20070512151532@example.net", mail.references
271 assert_include "redmine.message-1.20070512151532@example.net", mail.references
262 assert_select_email do
272 assert_select_email do
263 # link to the message
273 # link to the message
264 assert_select "a[href=?]",
274 assert_select "a[href=?]",
265 "http://mydomain.foo/boards/#{message.board.id}/topics/#{message.id}",
275 "http://mydomain.foo/boards/#{message.board.id}/topics/#{message.id}",
266 :text => message.subject
276 :text => message.subject
267 end
277 end
268 end
278 end
269
279
270 def test_reply_posted_message_id
280 def test_reply_posted_message_id
271 message = Message.find(3)
281 message = Message.find(3)
272 Mailer.message_posted(message).deliver
282 Mailer.message_posted(message).deliver
273 mail = last_email
283 mail = last_email
274 assert_match /^redmine\.message-3\.\d+\.[a-f0-9]+@example\.net/, mail.message_id
284 assert_match /^redmine\.message-3\.\d+\.[a-f0-9]+@example\.net/, mail.message_id
275 assert_include "redmine.message-1.20070512151532@example.net", mail.references
285 assert_include "redmine.message-1.20070512151532@example.net", mail.references
276 assert_select_email do
286 assert_select_email do
277 # link to the reply
287 # link to the reply
278 assert_select "a[href=?]",
288 assert_select "a[href=?]",
279 "http://mydomain.foo/boards/#{message.board.id}/topics/#{message.root.id}?r=#{message.id}#message-#{message.id}",
289 "http://mydomain.foo/boards/#{message.board.id}/topics/#{message.root.id}?r=#{message.id}#message-#{message.id}",
280 :text => message.subject
290 :text => message.subject
281 end
291 end
282 end
292 end
283
293
284 test "#issue_add should notify project members" do
294 test "#issue_add should notify project members" do
285 issue = Issue.find(1)
295 issue = Issue.find(1)
286 assert Mailer.deliver_issue_add(issue)
296 assert Mailer.deliver_issue_add(issue)
287 assert last_email.bcc.include?('dlopper@somenet.foo')
297 assert last_email.bcc.include?('dlopper@somenet.foo')
288 end
298 end
289
299
290 test "#issue_add should not notify project members that are not allow to view the issue" do
300 test "#issue_add should not notify project members that are not allow to view the issue" do
291 issue = Issue.find(1)
301 issue = Issue.find(1)
292 Role.find(2).remove_permission!(:view_issues)
302 Role.find(2).remove_permission!(:view_issues)
293 assert Mailer.deliver_issue_add(issue)
303 assert Mailer.deliver_issue_add(issue)
294 assert !last_email.bcc.include?('dlopper@somenet.foo')
304 assert !last_email.bcc.include?('dlopper@somenet.foo')
295 end
305 end
296
306
297 test "#issue_add should notify issue watchers" do
307 test "#issue_add should notify issue watchers" do
298 issue = Issue.find(1)
308 issue = Issue.find(1)
299 user = User.find(9)
309 user = User.find(9)
300 # minimal email notification options
310 # minimal email notification options
301 user.pref.no_self_notified = '1'
311 user.pref.no_self_notified = '1'
302 user.pref.save
312 user.pref.save
303 user.mail_notification = false
313 user.mail_notification = false
304 user.save
314 user.save
305
315
306 Watcher.create!(:watchable => issue, :user => user)
316 Watcher.create!(:watchable => issue, :user => user)
307 assert Mailer.deliver_issue_add(issue)
317 assert Mailer.deliver_issue_add(issue)
308 assert last_email.bcc.include?(user.mail)
318 assert last_email.bcc.include?(user.mail)
309 end
319 end
310
320
311 test "#issue_add should not notify watchers not allowed to view the issue" do
321 test "#issue_add should not notify watchers not allowed to view the issue" do
312 issue = Issue.find(1)
322 issue = Issue.find(1)
313 user = User.find(9)
323 user = User.find(9)
314 Watcher.create!(:watchable => issue, :user => user)
324 Watcher.create!(:watchable => issue, :user => user)
315 Role.non_member.remove_permission!(:view_issues)
325 Role.non_member.remove_permission!(:view_issues)
316 assert Mailer.deliver_issue_add(issue)
326 assert Mailer.deliver_issue_add(issue)
317 assert !last_email.bcc.include?(user.mail)
327 assert !last_email.bcc.include?(user.mail)
318 end
328 end
319
329
320 def test_issue_add_should_include_enabled_fields
330 def test_issue_add_should_include_enabled_fields
321 Setting.default_language = 'en'
331 Setting.default_language = 'en'
322 issue = Issue.find(2)
332 issue = Issue.find(2)
323 assert Mailer.deliver_issue_add(issue)
333 assert Mailer.deliver_issue_add(issue)
324 assert_mail_body_match '* Target version: 1.0', last_email
334 assert_mail_body_match '* Target version: 1.0', last_email
325 assert_select_email do
335 assert_select_email do
326 assert_select 'li', :text => 'Target version: 1.0'
336 assert_select 'li', :text => 'Target version: 1.0'
327 end
337 end
328 end
338 end
329
339
330 def test_issue_add_should_not_include_disabled_fields
340 def test_issue_add_should_not_include_disabled_fields
331 Setting.default_language = 'en'
341 Setting.default_language = 'en'
332 issue = Issue.find(2)
342 issue = Issue.find(2)
333 tracker = issue.tracker
343 tracker = issue.tracker
334 tracker.core_fields -= ['fixed_version_id']
344 tracker.core_fields -= ['fixed_version_id']
335 tracker.save!
345 tracker.save!
336 assert Mailer.deliver_issue_add(issue)
346 assert Mailer.deliver_issue_add(issue)
337 assert_mail_body_no_match 'Target version', last_email
347 assert_mail_body_no_match 'Target version', last_email
338 assert_select_email do
348 assert_select_email do
339 assert_select 'li', :text => /Target version/, :count => 0
349 assert_select 'li', :text => /Target version/, :count => 0
340 end
350 end
341 end
351 end
342
352
343 # test mailer methods for each language
353 # test mailer methods for each language
344 def test_issue_add
354 def test_issue_add
345 issue = Issue.find(1)
355 issue = Issue.find(1)
346 valid_languages.each do |lang|
356 valid_languages.each do |lang|
347 Setting.default_language = lang.to_s
357 Setting.default_language = lang.to_s
348 assert Mailer.deliver_issue_add(issue)
358 assert Mailer.deliver_issue_add(issue)
349 end
359 end
350 end
360 end
351
361
352 def test_issue_edit
362 def test_issue_edit
353 journal = Journal.find(1)
363 journal = Journal.find(1)
354 valid_languages.each do |lang|
364 valid_languages.each do |lang|
355 Setting.default_language = lang.to_s
365 Setting.default_language = lang.to_s
356 assert Mailer.deliver_issue_edit(journal)
366 assert Mailer.deliver_issue_edit(journal)
357 end
367 end
358 end
368 end
359
369
360 def test_issue_edit_should_send_private_notes_to_users_with_permission_only
370 def test_issue_edit_should_send_private_notes_to_users_with_permission_only
361 journal = Journal.find(1)
371 journal = Journal.find(1)
362 journal.private_notes = true
372 journal.private_notes = true
363 journal.save!
373 journal.save!
364
374
365 Role.find(2).add_permission! :view_private_notes
375 Role.find(2).add_permission! :view_private_notes
366 Mailer.deliver_issue_edit(journal)
376 Mailer.deliver_issue_edit(journal)
367 assert_equal %w(dlopper@somenet.foo jsmith@somenet.foo), ActionMailer::Base.deliveries.last.bcc.sort
377 assert_equal %w(dlopper@somenet.foo jsmith@somenet.foo), ActionMailer::Base.deliveries.last.bcc.sort
368
378
369 Role.find(2).remove_permission! :view_private_notes
379 Role.find(2).remove_permission! :view_private_notes
370 Mailer.deliver_issue_edit(journal)
380 Mailer.deliver_issue_edit(journal)
371 assert_equal %w(jsmith@somenet.foo), ActionMailer::Base.deliveries.last.bcc.sort
381 assert_equal %w(jsmith@somenet.foo), ActionMailer::Base.deliveries.last.bcc.sort
372 end
382 end
373
383
374 def test_issue_edit_should_send_private_notes_to_watchers_with_permission_only
384 def test_issue_edit_should_send_private_notes_to_watchers_with_permission_only
375 Issue.find(1).set_watcher(User.find_by_login('someone'))
385 Issue.find(1).set_watcher(User.find_by_login('someone'))
376 journal = Journal.find(1)
386 journal = Journal.find(1)
377 journal.private_notes = true
387 journal.private_notes = true
378 journal.save!
388 journal.save!
379
389
380 Role.non_member.add_permission! :view_private_notes
390 Role.non_member.add_permission! :view_private_notes
381 Mailer.deliver_issue_edit(journal)
391 Mailer.deliver_issue_edit(journal)
382 assert_include 'someone@foo.bar', ActionMailer::Base.deliveries.last.bcc.sort
392 assert_include 'someone@foo.bar', ActionMailer::Base.deliveries.last.bcc.sort
383
393
384 Role.non_member.remove_permission! :view_private_notes
394 Role.non_member.remove_permission! :view_private_notes
385 Mailer.deliver_issue_edit(journal)
395 Mailer.deliver_issue_edit(journal)
386 assert_not_include 'someone@foo.bar', ActionMailer::Base.deliveries.last.bcc.sort
396 assert_not_include 'someone@foo.bar', ActionMailer::Base.deliveries.last.bcc.sort
387 end
397 end
388
398
389 def test_issue_edit_should_mark_private_notes
399 def test_issue_edit_should_mark_private_notes
390 journal = Journal.find(2)
400 journal = Journal.find(2)
391 journal.private_notes = true
401 journal.private_notes = true
392 journal.save!
402 journal.save!
393
403
394 with_settings :default_language => 'en' do
404 with_settings :default_language => 'en' do
395 Mailer.deliver_issue_edit(journal)
405 Mailer.deliver_issue_edit(journal)
396 end
406 end
397 assert_mail_body_match '(Private notes)', last_email
407 assert_mail_body_match '(Private notes)', last_email
398 end
408 end
399
409
400 def test_issue_edit_with_relation_should_notify_users_who_can_see_the_related_issue
410 def test_issue_edit_with_relation_should_notify_users_who_can_see_the_related_issue
401 issue = Issue.generate!
411 issue = Issue.generate!
402 private_issue = Issue.generate!(:is_private => true)
412 private_issue = Issue.generate!(:is_private => true)
403 IssueRelation.create!(:issue_from => issue, :issue_to => private_issue, :relation_type => 'relates')
413 IssueRelation.create!(:issue_from => issue, :issue_to => private_issue, :relation_type => 'relates')
404 issue.reload
414 issue.reload
405 assert_equal 1, issue.journals.size
415 assert_equal 1, issue.journals.size
406 journal = issue.journals.first
416 journal = issue.journals.first
407 ActionMailer::Base.deliveries.clear
417 ActionMailer::Base.deliveries.clear
408
418
409 Mailer.deliver_issue_edit(journal)
419 Mailer.deliver_issue_edit(journal)
410 last_email.bcc.each do |email|
420 last_email.bcc.each do |email|
411 user = User.find_by_mail(email)
421 user = User.find_by_mail(email)
412 assert private_issue.visible?(user), "Issue was not visible to #{user}"
422 assert private_issue.visible?(user), "Issue was not visible to #{user}"
413 end
423 end
414 end
424 end
415
425
416 def test_document_added
426 def test_document_added
417 document = Document.find(1)
427 document = Document.find(1)
418 valid_languages.each do |lang|
428 valid_languages.each do |lang|
419 Setting.default_language = lang.to_s
429 Setting.default_language = lang.to_s
420 assert Mailer.document_added(document).deliver
430 assert Mailer.document_added(document).deliver
421 end
431 end
422 end
432 end
423
433
424 def test_attachments_added
434 def test_attachments_added
425 attachements = [ Attachment.find_by_container_type('Document') ]
435 attachements = [ Attachment.find_by_container_type('Document') ]
426 valid_languages.each do |lang|
436 valid_languages.each do |lang|
427 Setting.default_language = lang.to_s
437 Setting.default_language = lang.to_s
428 assert Mailer.attachments_added(attachements).deliver
438 assert Mailer.attachments_added(attachements).deliver
429 end
439 end
430 end
440 end
431
441
432 def test_version_file_added
442 def test_version_file_added
433 attachements = [ Attachment.find_by_container_type('Version') ]
443 attachements = [ Attachment.find_by_container_type('Version') ]
434 assert Mailer.attachments_added(attachements).deliver
444 assert Mailer.attachments_added(attachements).deliver
435 assert_not_nil last_email.bcc
445 assert_not_nil last_email.bcc
436 assert last_email.bcc.any?
446 assert last_email.bcc.any?
437 assert_select_email do
447 assert_select_email do
438 assert_select "a[href=?]", "http://mydomain.foo/projects/ecookbook/files"
448 assert_select "a[href=?]", "http://mydomain.foo/projects/ecookbook/files"
439 end
449 end
440 end
450 end
441
451
442 def test_project_file_added
452 def test_project_file_added
443 attachements = [ Attachment.find_by_container_type('Project') ]
453 attachements = [ Attachment.find_by_container_type('Project') ]
444 assert Mailer.attachments_added(attachements).deliver
454 assert Mailer.attachments_added(attachements).deliver
445 assert_not_nil last_email.bcc
455 assert_not_nil last_email.bcc
446 assert last_email.bcc.any?
456 assert last_email.bcc.any?
447 assert_select_email do
457 assert_select_email do
448 assert_select "a[href=?]", "http://mydomain.foo/projects/ecookbook/files"
458 assert_select "a[href=?]", "http://mydomain.foo/projects/ecookbook/files"
449 end
459 end
450 end
460 end
451
461
452 def test_news_added
462 def test_news_added
453 news = News.first
463 news = News.first
454 valid_languages.each do |lang|
464 valid_languages.each do |lang|
455 Setting.default_language = lang.to_s
465 Setting.default_language = lang.to_s
456 assert Mailer.news_added(news).deliver
466 assert Mailer.news_added(news).deliver
457 end
467 end
458 end
468 end
459
469
460 def test_news_comment_added
470 def test_news_comment_added
461 comment = Comment.find(2)
471 comment = Comment.find(2)
462 valid_languages.each do |lang|
472 valid_languages.each do |lang|
463 Setting.default_language = lang.to_s
473 Setting.default_language = lang.to_s
464 assert Mailer.news_comment_added(comment).deliver
474 assert Mailer.news_comment_added(comment).deliver
465 end
475 end
466 end
476 end
467
477
468 def test_message_posted
478 def test_message_posted
469 message = Message.first
479 message = Message.first
470 recipients = ([message.root] + message.root.children).collect {|m| m.author.mail if m.author}
480 recipients = ([message.root] + message.root.children).collect {|m| m.author.mail if m.author}
471 recipients = recipients.compact.uniq
481 recipients = recipients.compact.uniq
472 valid_languages.each do |lang|
482 valid_languages.each do |lang|
473 Setting.default_language = lang.to_s
483 Setting.default_language = lang.to_s
474 assert Mailer.message_posted(message).deliver
484 assert Mailer.message_posted(message).deliver
475 end
485 end
476 end
486 end
477
487
478 def test_wiki_content_added
488 def test_wiki_content_added
479 content = WikiContent.find(1)
489 content = WikiContent.find(1)
480 valid_languages.each do |lang|
490 valid_languages.each do |lang|
481 Setting.default_language = lang.to_s
491 Setting.default_language = lang.to_s
482 assert_difference 'ActionMailer::Base.deliveries.size' do
492 assert_difference 'ActionMailer::Base.deliveries.size' do
483 assert Mailer.wiki_content_added(content).deliver
493 assert Mailer.wiki_content_added(content).deliver
484 assert_select_email do
494 assert_select_email do
485 assert_select 'a[href=?]',
495 assert_select 'a[href=?]',
486 'http://mydomain.foo/projects/ecookbook/wiki/CookBook_documentation',
496 'http://mydomain.foo/projects/ecookbook/wiki/CookBook_documentation',
487 :text => 'CookBook documentation'
497 :text => 'CookBook documentation'
488 end
498 end
489 end
499 end
490 end
500 end
491 end
501 end
492
502
493 def test_wiki_content_updated
503 def test_wiki_content_updated
494 content = WikiContent.find(1)
504 content = WikiContent.find(1)
495 valid_languages.each do |lang|
505 valid_languages.each do |lang|
496 Setting.default_language = lang.to_s
506 Setting.default_language = lang.to_s
497 assert_difference 'ActionMailer::Base.deliveries.size' do
507 assert_difference 'ActionMailer::Base.deliveries.size' do
498 assert Mailer.wiki_content_updated(content).deliver
508 assert Mailer.wiki_content_updated(content).deliver
499 assert_select_email do
509 assert_select_email do
500 assert_select 'a[href=?]',
510 assert_select 'a[href=?]',
501 'http://mydomain.foo/projects/ecookbook/wiki/CookBook_documentation',
511 'http://mydomain.foo/projects/ecookbook/wiki/CookBook_documentation',
502 :text => 'CookBook documentation'
512 :text => 'CookBook documentation'
503 end
513 end
504 end
514 end
505 end
515 end
506 end
516 end
507
517
508 def test_account_information
518 def test_account_information
509 user = User.find(2)
519 user = User.find(2)
510 valid_languages.each do |lang|
520 valid_languages.each do |lang|
511 user.update_attribute :language, lang.to_s
521 user.update_attribute :language, lang.to_s
512 user.reload
522 user.reload
513 assert Mailer.account_information(user, 'pAsswORd').deliver
523 assert Mailer.account_information(user, 'pAsswORd').deliver
514 end
524 end
515 end
525 end
516
526
517 def test_lost_password
527 def test_lost_password
518 token = Token.find(2)
528 token = Token.find(2)
519 valid_languages.each do |lang|
529 valid_languages.each do |lang|
520 token.user.update_attribute :language, lang.to_s
530 token.user.update_attribute :language, lang.to_s
521 token.reload
531 token.reload
522 assert Mailer.lost_password(token).deliver
532 assert Mailer.lost_password(token).deliver
523 end
533 end
524 end
534 end
525
535
526 def test_register
536 def test_register
527 token = Token.find(1)
537 token = Token.find(1)
528 Setting.host_name = 'redmine.foo'
538 Setting.host_name = 'redmine.foo'
529 Setting.protocol = 'https'
539 Setting.protocol = 'https'
530
540
531 valid_languages.each do |lang|
541 valid_languages.each do |lang|
532 token.user.update_attribute :language, lang.to_s
542 token.user.update_attribute :language, lang.to_s
533 token.reload
543 token.reload
534 ActionMailer::Base.deliveries.clear
544 ActionMailer::Base.deliveries.clear
535 assert Mailer.register(token).deliver
545 assert Mailer.register(token).deliver
536 mail = last_email
546 mail = last_email
537 assert_select_email do
547 assert_select_email do
538 assert_select "a[href=?]",
548 assert_select "a[href=?]",
539 "https://redmine.foo/account/activate?token=#{token.value}",
549 "https://redmine.foo/account/activate?token=#{token.value}",
540 :text => "https://redmine.foo/account/activate?token=#{token.value}"
550 :text => "https://redmine.foo/account/activate?token=#{token.value}"
541 end
551 end
542 end
552 end
543 end
553 end
544
554
545 def test_test
555 def test_test
546 user = User.find(1)
556 user = User.find(1)
547 valid_languages.each do |lang|
557 valid_languages.each do |lang|
548 user.update_attribute :language, lang.to_s
558 user.update_attribute :language, lang.to_s
549 assert Mailer.test_email(user).deliver
559 assert Mailer.test_email(user).deliver
550 end
560 end
551 end
561 end
552
562
553 def test_reminders
563 def test_reminders
554 Mailer.reminders(:days => 42)
564 Mailer.reminders(:days => 42)
555 assert_equal 1, ActionMailer::Base.deliveries.size
565 assert_equal 1, ActionMailer::Base.deliveries.size
556 mail = last_email
566 mail = last_email
557 assert mail.bcc.include?('dlopper@somenet.foo')
567 assert mail.bcc.include?('dlopper@somenet.foo')
558 assert_mail_body_match 'Bug #3: Error 281 when updating a recipe', mail
568 assert_mail_body_match 'Bug #3: Error 281 when updating a recipe', mail
559 assert_equal '1 issue(s) due in the next 42 days', mail.subject
569 assert_equal '1 issue(s) due in the next 42 days', mail.subject
560 end
570 end
561
571
562 def test_reminders_should_not_include_closed_issues
572 def test_reminders_should_not_include_closed_issues
563 with_settings :default_language => 'en' do
573 with_settings :default_language => 'en' do
564 Issue.create!(:project_id => 1, :tracker_id => 1, :status_id => 5,
574 Issue.create!(:project_id => 1, :tracker_id => 1, :status_id => 5,
565 :subject => 'Closed issue', :assigned_to_id => 3,
575 :subject => 'Closed issue', :assigned_to_id => 3,
566 :due_date => 5.days.from_now,
576 :due_date => 5.days.from_now,
567 :author_id => 2)
577 :author_id => 2)
568 ActionMailer::Base.deliveries.clear
578 ActionMailer::Base.deliveries.clear
569
579
570 Mailer.reminders(:days => 42)
580 Mailer.reminders(:days => 42)
571 assert_equal 1, ActionMailer::Base.deliveries.size
581 assert_equal 1, ActionMailer::Base.deliveries.size
572 mail = last_email
582 mail = last_email
573 assert mail.bcc.include?('dlopper@somenet.foo')
583 assert mail.bcc.include?('dlopper@somenet.foo')
574 assert_mail_body_no_match 'Closed issue', mail
584 assert_mail_body_no_match 'Closed issue', mail
575 end
585 end
576 end
586 end
577
587
578 def test_reminders_for_users
588 def test_reminders_for_users
579 Mailer.reminders(:days => 42, :users => ['5'])
589 Mailer.reminders(:days => 42, :users => ['5'])
580 assert_equal 0, ActionMailer::Base.deliveries.size # No mail for dlopper
590 assert_equal 0, ActionMailer::Base.deliveries.size # No mail for dlopper
581 Mailer.reminders(:days => 42, :users => ['3'])
591 Mailer.reminders(:days => 42, :users => ['3'])
582 assert_equal 1, ActionMailer::Base.deliveries.size # No mail for dlopper
592 assert_equal 1, ActionMailer::Base.deliveries.size # No mail for dlopper
583 mail = last_email
593 mail = last_email
584 assert mail.bcc.include?('dlopper@somenet.foo')
594 assert mail.bcc.include?('dlopper@somenet.foo')
585 assert_mail_body_match 'Bug #3: Error 281 when updating a recipe', mail
595 assert_mail_body_match 'Bug #3: Error 281 when updating a recipe', mail
586 end
596 end
587
597
588 def test_reminder_should_include_issues_assigned_to_groups
598 def test_reminder_should_include_issues_assigned_to_groups
589 with_settings :default_language => 'en' do
599 with_settings :default_language => 'en' do
590 group = Group.generate!
600 group = Group.generate!
591 group.users << User.find(2)
601 group.users << User.find(2)
592 group.users << User.find(3)
602 group.users << User.find(3)
593
603
594 Issue.create!(:project_id => 1, :tracker_id => 1, :status_id => 1,
604 Issue.create!(:project_id => 1, :tracker_id => 1, :status_id => 1,
595 :subject => 'Assigned to group', :assigned_to => group,
605 :subject => 'Assigned to group', :assigned_to => group,
596 :due_date => 5.days.from_now,
606 :due_date => 5.days.from_now,
597 :author_id => 2)
607 :author_id => 2)
598 ActionMailer::Base.deliveries.clear
608 ActionMailer::Base.deliveries.clear
599
609
600 Mailer.reminders(:days => 7)
610 Mailer.reminders(:days => 7)
601 assert_equal 2, ActionMailer::Base.deliveries.size
611 assert_equal 2, ActionMailer::Base.deliveries.size
602 assert_equal %w(dlopper@somenet.foo jsmith@somenet.foo), ActionMailer::Base.deliveries.map(&:bcc).flatten.sort
612 assert_equal %w(dlopper@somenet.foo jsmith@somenet.foo), ActionMailer::Base.deliveries.map(&:bcc).flatten.sort
603 ActionMailer::Base.deliveries.each do |mail|
613 ActionMailer::Base.deliveries.each do |mail|
604 assert_mail_body_match 'Assigned to group', mail
614 assert_mail_body_match 'Assigned to group', mail
605 end
615 end
606 end
616 end
607 end
617 end
608
618
609 def test_mailer_should_not_change_locale
619 def test_mailer_should_not_change_locale
610 Setting.default_language = 'en'
620 Setting.default_language = 'en'
611 # Set current language to italian
621 # Set current language to italian
612 set_language_if_valid 'it'
622 set_language_if_valid 'it'
613 # Send an email to a french user
623 # Send an email to a french user
614 user = User.find(1)
624 user = User.find(1)
615 user.language = 'fr'
625 user.language = 'fr'
616 Mailer.account_activated(user).deliver
626 Mailer.account_activated(user).deliver
617 mail = last_email
627 mail = last_email
618 assert_mail_body_match 'Votre compte', mail
628 assert_mail_body_match 'Votre compte', mail
619
629
620 assert_equal :it, current_language
630 assert_equal :it, current_language
621 end
631 end
622
632
623 def test_with_deliveries_off
633 def test_with_deliveries_off
624 Mailer.with_deliveries false do
634 Mailer.with_deliveries false do
625 Mailer.test_email(User.find(1)).deliver
635 Mailer.test_email(User.find(1)).deliver
626 end
636 end
627 assert ActionMailer::Base.deliveries.empty?
637 assert ActionMailer::Base.deliveries.empty?
628 # should restore perform_deliveries
638 # should restore perform_deliveries
629 assert ActionMailer::Base.perform_deliveries
639 assert ActionMailer::Base.perform_deliveries
630 end
640 end
631
641
632 def test_layout_should_include_the_emails_header
642 def test_layout_should_include_the_emails_header
633 with_settings :emails_header => "*Header content*" do
643 with_settings :emails_header => "*Header content*" do
634 with_settings :plain_text_mail => 0 do
644 with_settings :plain_text_mail => 0 do
635 assert Mailer.test_email(User.find(1)).deliver
645 assert Mailer.test_email(User.find(1)).deliver
636 assert_select_email do
646 assert_select_email do
637 assert_select ".header" do
647 assert_select ".header" do
638 assert_select "strong", :text => "Header content"
648 assert_select "strong", :text => "Header content"
639 end
649 end
640 end
650 end
641 end
651 end
642 with_settings :plain_text_mail => 1 do
652 with_settings :plain_text_mail => 1 do
643 assert Mailer.test_email(User.find(1)).deliver
653 assert Mailer.test_email(User.find(1)).deliver
644 mail = last_email
654 mail = last_email
645 assert_not_nil mail
655 assert_not_nil mail
646 assert_include "*Header content*", mail.body.decoded
656 assert_include "*Header content*", mail.body.decoded
647 end
657 end
648 end
658 end
649 end
659 end
650
660
651 def test_layout_should_not_include_empty_emails_header
661 def test_layout_should_not_include_empty_emails_header
652 with_settings :emails_header => "", :plain_text_mail => 0 do
662 with_settings :emails_header => "", :plain_text_mail => 0 do
653 assert Mailer.test_email(User.find(1)).deliver
663 assert Mailer.test_email(User.find(1)).deliver
654 assert_select_email do
664 assert_select_email do
655 assert_select ".header", false
665 assert_select ".header", false
656 end
666 end
657 end
667 end
658 end
668 end
659
669
660 def test_layout_should_include_the_emails_footer
670 def test_layout_should_include_the_emails_footer
661 with_settings :emails_footer => "*Footer content*" do
671 with_settings :emails_footer => "*Footer content*" do
662 with_settings :plain_text_mail => 0 do
672 with_settings :plain_text_mail => 0 do
663 assert Mailer.test_email(User.find(1)).deliver
673 assert Mailer.test_email(User.find(1)).deliver
664 assert_select_email do
674 assert_select_email do
665 assert_select ".footer" do
675 assert_select ".footer" do
666 assert_select "strong", :text => "Footer content"
676 assert_select "strong", :text => "Footer content"
667 end
677 end
668 end
678 end
669 end
679 end
670 with_settings :plain_text_mail => 1 do
680 with_settings :plain_text_mail => 1 do
671 assert Mailer.test_email(User.find(1)).deliver
681 assert Mailer.test_email(User.find(1)).deliver
672 mail = last_email
682 mail = last_email
673 assert_not_nil mail
683 assert_not_nil mail
674 assert_include "\n-- \n", mail.body.decoded
684 assert_include "\n-- \n", mail.body.decoded
675 assert_include "*Footer content*", mail.body.decoded
685 assert_include "*Footer content*", mail.body.decoded
676 end
686 end
677 end
687 end
678 end
688 end
679
689
680 def test_layout_should_not_include_empty_emails_footer
690 def test_layout_should_not_include_empty_emails_footer
681 with_settings :emails_footer => "" do
691 with_settings :emails_footer => "" do
682 with_settings :plain_text_mail => 0 do
692 with_settings :plain_text_mail => 0 do
683 assert Mailer.test_email(User.find(1)).deliver
693 assert Mailer.test_email(User.find(1)).deliver
684 assert_select_email do
694 assert_select_email do
685 assert_select ".footer", false
695 assert_select ".footer", false
686 end
696 end
687 end
697 end
688 with_settings :plain_text_mail => 1 do
698 with_settings :plain_text_mail => 1 do
689 assert Mailer.test_email(User.find(1)).deliver
699 assert Mailer.test_email(User.find(1)).deliver
690 mail = last_email
700 mail = last_email
691 assert_not_nil mail
701 assert_not_nil mail
692 assert_not_include "\n-- \n", mail.body.decoded
702 assert_not_include "\n-- \n", mail.body.decoded
693 end
703 end
694 end
704 end
695 end
705 end
696
706
697 def test_should_escape_html_templates_only
707 def test_should_escape_html_templates_only
698 Issue.generate!(:project_id => 1, :tracker_id => 1, :subject => 'Subject with a <tag>')
708 Issue.generate!(:project_id => 1, :tracker_id => 1, :subject => 'Subject with a <tag>')
699 mail = last_email
709 mail = last_email
700 assert_equal 2, mail.parts.size
710 assert_equal 2, mail.parts.size
701 assert_include '<tag>', text_part.body.encoded
711 assert_include '<tag>', text_part.body.encoded
702 assert_include '&lt;tag&gt;', html_part.body.encoded
712 assert_include '&lt;tag&gt;', html_part.body.encoded
703 end
713 end
704
714
705 def test_should_raise_delivery_errors_when_raise_delivery_errors_is_true
715 def test_should_raise_delivery_errors_when_raise_delivery_errors_is_true
706 mail = Mailer.test_email(User.find(1))
716 mail = Mailer.test_email(User.find(1))
707 mail.delivery_method.stubs(:deliver!).raises(Exception.new("delivery error"))
717 mail.delivery_method.stubs(:deliver!).raises(Exception.new("delivery error"))
708
718
709 ActionMailer::Base.raise_delivery_errors = true
719 ActionMailer::Base.raise_delivery_errors = true
710 assert_raise Exception, "delivery error" do
720 assert_raise Exception, "delivery error" do
711 mail.deliver
721 mail.deliver
712 end
722 end
713 ensure
723 ensure
714 ActionMailer::Base.raise_delivery_errors = false
724 ActionMailer::Base.raise_delivery_errors = false
715 end
725 end
716
726
717 def test_should_log_delivery_errors_when_raise_delivery_errors_is_false
727 def test_should_log_delivery_errors_when_raise_delivery_errors_is_false
718 mail = Mailer.test_email(User.find(1))
728 mail = Mailer.test_email(User.find(1))
719 mail.delivery_method.stubs(:deliver!).raises(Exception.new("delivery error"))
729 mail.delivery_method.stubs(:deliver!).raises(Exception.new("delivery error"))
720
730
721 Rails.logger.expects(:error).with("Email delivery error: delivery error")
731 Rails.logger.expects(:error).with("Email delivery error: delivery error")
722 ActionMailer::Base.raise_delivery_errors = false
732 ActionMailer::Base.raise_delivery_errors = false
723 assert_nothing_raised do
733 assert_nothing_raised do
724 mail.deliver
734 mail.deliver
725 end
735 end
726 end
736 end
727
737
728 def test_mail_should_return_a_mail_message
738 def test_mail_should_return_a_mail_message
729 assert_kind_of ::Mail::Message, Mailer.test_email(User.find(1))
739 assert_kind_of ::Mail::Message, Mailer.test_email(User.find(1))
730 end
740 end
731
741
732 private
742 private
733
743
734 def last_email
744 def last_email
735 mail = ActionMailer::Base.deliveries.last
745 mail = ActionMailer::Base.deliveries.last
736 assert_not_nil mail
746 assert_not_nil mail
737 mail
747 mail
738 end
748 end
739
749
740 def text_part
750 def text_part
741 last_email.parts.detect {|part| part.content_type.include?('text/plain')}
751 last_email.parts.detect {|part| part.content_type.include?('text/plain')}
742 end
752 end
743
753
744 def html_part
754 def html_part
745 last_email.parts.detect {|part| part.content_type.include?('text/html')}
755 last_email.parts.detect {|part| part.content_type.include?('text/html')}
746 end
756 end
747 end
757 end
General Comments 0
You need to be logged in to leave comments. Login now