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