##// END OF EJS Templates
Adds tracker name to Redmine issue link titles (#13946)....
Jean-Philippe Lang -
r14238:a7b6ad9bb5ba
parent child
Show More
@@ -1,1335 +1,1335
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 require 'forwardable'
20 require 'forwardable'
21 require 'cgi'
21 require 'cgi'
22
22
23 module ApplicationHelper
23 module ApplicationHelper
24 include Redmine::WikiFormatting::Macros::Definitions
24 include Redmine::WikiFormatting::Macros::Definitions
25 include Redmine::I18n
25 include Redmine::I18n
26 include GravatarHelper::PublicMethods
26 include GravatarHelper::PublicMethods
27 include Redmine::Pagination::Helper
27 include Redmine::Pagination::Helper
28 include Redmine::SudoMode::Helper
28 include Redmine::SudoMode::Helper
29
29
30 extend Forwardable
30 extend Forwardable
31 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
31 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
32
32
33 # Return true if user is authorized for controller/action, otherwise false
33 # Return true if user is authorized for controller/action, otherwise false
34 def authorize_for(controller, action)
34 def authorize_for(controller, action)
35 User.current.allowed_to?({:controller => controller, :action => action}, @project)
35 User.current.allowed_to?({:controller => controller, :action => action}, @project)
36 end
36 end
37
37
38 # Display a link if user is authorized
38 # Display a link if user is authorized
39 #
39 #
40 # @param [String] name Anchor text (passed to link_to)
40 # @param [String] name Anchor text (passed to link_to)
41 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
41 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
42 # @param [optional, Hash] html_options Options passed to link_to
42 # @param [optional, Hash] html_options Options passed to link_to
43 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
43 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
44 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
44 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
45 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
45 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
46 end
46 end
47
47
48 # Displays a link to user's account page if active
48 # Displays a link to user's account page if active
49 def link_to_user(user, options={})
49 def link_to_user(user, options={})
50 if user.is_a?(User)
50 if user.is_a?(User)
51 name = h(user.name(options[:format]))
51 name = h(user.name(options[:format]))
52 if user.active? || (User.current.admin? && user.logged?)
52 if user.active? || (User.current.admin? && user.logged?)
53 link_to name, user_path(user), :class => user.css_classes
53 link_to name, user_path(user), :class => user.css_classes
54 else
54 else
55 name
55 name
56 end
56 end
57 else
57 else
58 h(user.to_s)
58 h(user.to_s)
59 end
59 end
60 end
60 end
61
61
62 # Displays a link to +issue+ with its subject.
62 # Displays a link to +issue+ with its subject.
63 # Examples:
63 # Examples:
64 #
64 #
65 # link_to_issue(issue) # => Defect #6: This is the subject
65 # link_to_issue(issue) # => Defect #6: This is the subject
66 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
66 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
67 # link_to_issue(issue, :subject => false) # => Defect #6
67 # link_to_issue(issue, :subject => false) # => Defect #6
68 # link_to_issue(issue, :project => true) # => Foo - Defect #6
68 # link_to_issue(issue, :project => true) # => Foo - Defect #6
69 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
69 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
70 #
70 #
71 def link_to_issue(issue, options={})
71 def link_to_issue(issue, options={})
72 title = nil
72 title = nil
73 subject = nil
73 subject = nil
74 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
74 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
75 if options[:subject] == false
75 if options[:subject] == false
76 title = issue.subject.truncate(60)
76 title = issue.subject.truncate(60)
77 else
77 else
78 subject = issue.subject
78 subject = issue.subject
79 if truncate_length = options[:truncate]
79 if truncate_length = options[:truncate]
80 subject = subject.truncate(truncate_length)
80 subject = subject.truncate(truncate_length)
81 end
81 end
82 end
82 end
83 only_path = options[:only_path].nil? ? true : options[:only_path]
83 only_path = options[:only_path].nil? ? true : options[:only_path]
84 s = link_to(text, issue_url(issue, :only_path => only_path),
84 s = link_to(text, issue_url(issue, :only_path => only_path),
85 :class => issue.css_classes, :title => title)
85 :class => issue.css_classes, :title => title)
86 s << h(": #{subject}") if subject
86 s << h(": #{subject}") if subject
87 s = h("#{issue.project} - ") + s if options[:project]
87 s = h("#{issue.project} - ") + s if options[:project]
88 s
88 s
89 end
89 end
90
90
91 # Generates a link to an attachment.
91 # Generates a link to an attachment.
92 # Options:
92 # Options:
93 # * :text - Link text (default to attachment filename)
93 # * :text - Link text (default to attachment filename)
94 # * :download - Force download (default: false)
94 # * :download - Force download (default: false)
95 def link_to_attachment(attachment, options={})
95 def link_to_attachment(attachment, options={})
96 text = options.delete(:text) || attachment.filename
96 text = options.delete(:text) || attachment.filename
97 route_method = options.delete(:download) ? :download_named_attachment_url : :named_attachment_url
97 route_method = options.delete(:download) ? :download_named_attachment_url : :named_attachment_url
98 html_options = options.slice!(:only_path)
98 html_options = options.slice!(:only_path)
99 options[:only_path] = true unless options.key?(:only_path)
99 options[:only_path] = true unless options.key?(:only_path)
100 url = send(route_method, attachment, attachment.filename, options)
100 url = send(route_method, attachment, attachment.filename, options)
101 link_to text, url, html_options
101 link_to text, url, html_options
102 end
102 end
103
103
104 # Generates a link to a SCM revision
104 # Generates a link to a SCM revision
105 # Options:
105 # Options:
106 # * :text - Link text (default to the formatted revision)
106 # * :text - Link text (default to the formatted revision)
107 def link_to_revision(revision, repository, options={})
107 def link_to_revision(revision, repository, options={})
108 if repository.is_a?(Project)
108 if repository.is_a?(Project)
109 repository = repository.repository
109 repository = repository.repository
110 end
110 end
111 text = options.delete(:text) || format_revision(revision)
111 text = options.delete(:text) || format_revision(revision)
112 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
112 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
113 link_to(
113 link_to(
114 h(text),
114 h(text),
115 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
115 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
116 :title => l(:label_revision_id, format_revision(revision)),
116 :title => l(:label_revision_id, format_revision(revision)),
117 :accesskey => options[:accesskey]
117 :accesskey => options[:accesskey]
118 )
118 )
119 end
119 end
120
120
121 # Generates a link to a message
121 # Generates a link to a message
122 def link_to_message(message, options={}, html_options = nil)
122 def link_to_message(message, options={}, html_options = nil)
123 link_to(
123 link_to(
124 message.subject.truncate(60),
124 message.subject.truncate(60),
125 board_message_url(message.board_id, message.parent_id || message.id, {
125 board_message_url(message.board_id, message.parent_id || message.id, {
126 :r => (message.parent_id && message.id),
126 :r => (message.parent_id && message.id),
127 :anchor => (message.parent_id ? "message-#{message.id}" : nil),
127 :anchor => (message.parent_id ? "message-#{message.id}" : nil),
128 :only_path => true
128 :only_path => true
129 }.merge(options)),
129 }.merge(options)),
130 html_options
130 html_options
131 )
131 )
132 end
132 end
133
133
134 # Generates a link to a project if active
134 # Generates a link to a project if active
135 # Examples:
135 # Examples:
136 #
136 #
137 # link_to_project(project) # => link to the specified project overview
137 # link_to_project(project) # => link to the specified project overview
138 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
138 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
139 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
139 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
140 #
140 #
141 def link_to_project(project, options={}, html_options = nil)
141 def link_to_project(project, options={}, html_options = nil)
142 if project.archived?
142 if project.archived?
143 h(project.name)
143 h(project.name)
144 else
144 else
145 link_to project.name,
145 link_to project.name,
146 project_url(project, {:only_path => true}.merge(options)),
146 project_url(project, {:only_path => true}.merge(options)),
147 html_options
147 html_options
148 end
148 end
149 end
149 end
150
150
151 # Generates a link to a project settings if active
151 # Generates a link to a project settings if active
152 def link_to_project_settings(project, options={}, html_options=nil)
152 def link_to_project_settings(project, options={}, html_options=nil)
153 if project.active?
153 if project.active?
154 link_to project.name, settings_project_path(project, options), html_options
154 link_to project.name, settings_project_path(project, options), html_options
155 elsif project.archived?
155 elsif project.archived?
156 h(project.name)
156 h(project.name)
157 else
157 else
158 link_to project.name, project_path(project, options), html_options
158 link_to project.name, project_path(project, options), html_options
159 end
159 end
160 end
160 end
161
161
162 # Generates a link to a version
162 # Generates a link to a version
163 def link_to_version(version, options = {})
163 def link_to_version(version, options = {})
164 return '' unless version && version.is_a?(Version)
164 return '' unless version && version.is_a?(Version)
165 options = {:title => format_date(version.effective_date)}.merge(options)
165 options = {:title => format_date(version.effective_date)}.merge(options)
166 link_to_if version.visible?, format_version_name(version), version_path(version), options
166 link_to_if version.visible?, format_version_name(version), version_path(version), options
167 end
167 end
168
168
169 # Helper that formats object for html or text rendering
169 # Helper that formats object for html or text rendering
170 def format_object(object, html=true, &block)
170 def format_object(object, html=true, &block)
171 if block_given?
171 if block_given?
172 object = yield object
172 object = yield object
173 end
173 end
174 case object.class.name
174 case object.class.name
175 when 'Array'
175 when 'Array'
176 object.map {|o| format_object(o, html)}.join(', ').html_safe
176 object.map {|o| format_object(o, html)}.join(', ').html_safe
177 when 'Time'
177 when 'Time'
178 format_time(object)
178 format_time(object)
179 when 'Date'
179 when 'Date'
180 format_date(object)
180 format_date(object)
181 when 'Fixnum'
181 when 'Fixnum'
182 object.to_s
182 object.to_s
183 when 'Float'
183 when 'Float'
184 sprintf "%.2f", object
184 sprintf "%.2f", object
185 when 'User'
185 when 'User'
186 html ? link_to_user(object) : object.to_s
186 html ? link_to_user(object) : object.to_s
187 when 'Project'
187 when 'Project'
188 html ? link_to_project(object) : object.to_s
188 html ? link_to_project(object) : object.to_s
189 when 'Version'
189 when 'Version'
190 html ? link_to_version(object) : object.to_s
190 html ? link_to_version(object) : object.to_s
191 when 'TrueClass'
191 when 'TrueClass'
192 l(:general_text_Yes)
192 l(:general_text_Yes)
193 when 'FalseClass'
193 when 'FalseClass'
194 l(:general_text_No)
194 l(:general_text_No)
195 when 'Issue'
195 when 'Issue'
196 object.visible? && html ? link_to_issue(object) : "##{object.id}"
196 object.visible? && html ? link_to_issue(object) : "##{object.id}"
197 when 'CustomValue', 'CustomFieldValue'
197 when 'CustomValue', 'CustomFieldValue'
198 if object.custom_field
198 if object.custom_field
199 f = object.custom_field.format.formatted_custom_value(self, object, html)
199 f = object.custom_field.format.formatted_custom_value(self, object, html)
200 if f.nil? || f.is_a?(String)
200 if f.nil? || f.is_a?(String)
201 f
201 f
202 else
202 else
203 format_object(f, html, &block)
203 format_object(f, html, &block)
204 end
204 end
205 else
205 else
206 object.value.to_s
206 object.value.to_s
207 end
207 end
208 else
208 else
209 html ? h(object) : object.to_s
209 html ? h(object) : object.to_s
210 end
210 end
211 end
211 end
212
212
213 def wiki_page_path(page, options={})
213 def wiki_page_path(page, options={})
214 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
214 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
215 end
215 end
216
216
217 def thumbnail_tag(attachment)
217 def thumbnail_tag(attachment)
218 link_to image_tag(thumbnail_path(attachment)),
218 link_to image_tag(thumbnail_path(attachment)),
219 named_attachment_path(attachment, attachment.filename),
219 named_attachment_path(attachment, attachment.filename),
220 :title => attachment.filename
220 :title => attachment.filename
221 end
221 end
222
222
223 def toggle_link(name, id, options={})
223 def toggle_link(name, id, options={})
224 onclick = "$('##{id}').toggle(); "
224 onclick = "$('##{id}').toggle(); "
225 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
225 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
226 onclick << "return false;"
226 onclick << "return false;"
227 link_to(name, "#", :onclick => onclick)
227 link_to(name, "#", :onclick => onclick)
228 end
228 end
229
229
230 def format_activity_title(text)
230 def format_activity_title(text)
231 h(truncate_single_line_raw(text, 100))
231 h(truncate_single_line_raw(text, 100))
232 end
232 end
233
233
234 def format_activity_day(date)
234 def format_activity_day(date)
235 date == User.current.today ? l(:label_today).titleize : format_date(date)
235 date == User.current.today ? l(:label_today).titleize : format_date(date)
236 end
236 end
237
237
238 def format_activity_description(text)
238 def format_activity_description(text)
239 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
239 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
240 ).gsub(/[\r\n]+/, "<br />").html_safe
240 ).gsub(/[\r\n]+/, "<br />").html_safe
241 end
241 end
242
242
243 def format_version_name(version)
243 def format_version_name(version)
244 if version.project == @project
244 if version.project == @project
245 h(version)
245 h(version)
246 else
246 else
247 h("#{version.project} - #{version}")
247 h("#{version.project} - #{version}")
248 end
248 end
249 end
249 end
250
250
251 def due_date_distance_in_words(date)
251 def due_date_distance_in_words(date)
252 if date
252 if date
253 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
253 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
254 end
254 end
255 end
255 end
256
256
257 # Renders a tree of projects as a nested set of unordered lists
257 # Renders a tree of projects as a nested set of unordered lists
258 # The given collection may be a subset of the whole project tree
258 # The given collection may be a subset of the whole project tree
259 # (eg. some intermediate nodes are private and can not be seen)
259 # (eg. some intermediate nodes are private and can not be seen)
260 def render_project_nested_lists(projects, &block)
260 def render_project_nested_lists(projects, &block)
261 s = ''
261 s = ''
262 if projects.any?
262 if projects.any?
263 ancestors = []
263 ancestors = []
264 original_project = @project
264 original_project = @project
265 projects.sort_by(&:lft).each do |project|
265 projects.sort_by(&:lft).each do |project|
266 # set the project environment to please macros.
266 # set the project environment to please macros.
267 @project = project
267 @project = project
268 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
268 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
269 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
269 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
270 else
270 else
271 ancestors.pop
271 ancestors.pop
272 s << "</li>"
272 s << "</li>"
273 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
273 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
274 ancestors.pop
274 ancestors.pop
275 s << "</ul></li>\n"
275 s << "</ul></li>\n"
276 end
276 end
277 end
277 end
278 classes = (ancestors.empty? ? 'root' : 'child')
278 classes = (ancestors.empty? ? 'root' : 'child')
279 s << "<li class='#{classes}'><div class='#{classes}'>"
279 s << "<li class='#{classes}'><div class='#{classes}'>"
280 s << h(block_given? ? capture(project, &block) : project.name)
280 s << h(block_given? ? capture(project, &block) : project.name)
281 s << "</div>\n"
281 s << "</div>\n"
282 ancestors << project
282 ancestors << project
283 end
283 end
284 s << ("</li></ul>\n" * ancestors.size)
284 s << ("</li></ul>\n" * ancestors.size)
285 @project = original_project
285 @project = original_project
286 end
286 end
287 s.html_safe
287 s.html_safe
288 end
288 end
289
289
290 def render_page_hierarchy(pages, node=nil, options={})
290 def render_page_hierarchy(pages, node=nil, options={})
291 content = ''
291 content = ''
292 if pages[node]
292 if pages[node]
293 content << "<ul class=\"pages-hierarchy\">\n"
293 content << "<ul class=\"pages-hierarchy\">\n"
294 pages[node].each do |page|
294 pages[node].each do |page|
295 content << "<li>"
295 content << "<li>"
296 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
296 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
297 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
297 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
298 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
298 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
299 content << "</li>\n"
299 content << "</li>\n"
300 end
300 end
301 content << "</ul>\n"
301 content << "</ul>\n"
302 end
302 end
303 content.html_safe
303 content.html_safe
304 end
304 end
305
305
306 # Renders flash messages
306 # Renders flash messages
307 def render_flash_messages
307 def render_flash_messages
308 s = ''
308 s = ''
309 flash.each do |k,v|
309 flash.each do |k,v|
310 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
310 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
311 end
311 end
312 s.html_safe
312 s.html_safe
313 end
313 end
314
314
315 # Renders tabs and their content
315 # Renders tabs and their content
316 def render_tabs(tabs, selected=params[:tab])
316 def render_tabs(tabs, selected=params[:tab])
317 if tabs.any?
317 if tabs.any?
318 unless tabs.detect {|tab| tab[:name] == selected}
318 unless tabs.detect {|tab| tab[:name] == selected}
319 selected = nil
319 selected = nil
320 end
320 end
321 selected ||= tabs.first[:name]
321 selected ||= tabs.first[:name]
322 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
322 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
323 else
323 else
324 content_tag 'p', l(:label_no_data), :class => "nodata"
324 content_tag 'p', l(:label_no_data), :class => "nodata"
325 end
325 end
326 end
326 end
327
327
328 # Renders the project quick-jump box
328 # Renders the project quick-jump box
329 def render_project_jump_box
329 def render_project_jump_box
330 return unless User.current.logged?
330 return unless User.current.logged?
331 projects = User.current.projects.active.select(:id, :name, :identifier, :lft, :rgt).to_a
331 projects = User.current.projects.active.select(:id, :name, :identifier, :lft, :rgt).to_a
332 if projects.any?
332 if projects.any?
333 options =
333 options =
334 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
334 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
335 '<option value="" disabled="disabled">---</option>').html_safe
335 '<option value="" disabled="disabled">---</option>').html_safe
336
336
337 options << project_tree_options_for_select(projects, :selected => @project) do |p|
337 options << project_tree_options_for_select(projects, :selected => @project) do |p|
338 { :value => project_path(:id => p, :jump => current_menu_item) }
338 { :value => project_path(:id => p, :jump => current_menu_item) }
339 end
339 end
340
340
341 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
341 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
342 end
342 end
343 end
343 end
344
344
345 def project_tree_options_for_select(projects, options = {})
345 def project_tree_options_for_select(projects, options = {})
346 s = ''.html_safe
346 s = ''.html_safe
347 if blank_text = options[:include_blank]
347 if blank_text = options[:include_blank]
348 if blank_text == true
348 if blank_text == true
349 blank_text = '&nbsp;'.html_safe
349 blank_text = '&nbsp;'.html_safe
350 end
350 end
351 s << content_tag('option', blank_text, :value => '')
351 s << content_tag('option', blank_text, :value => '')
352 end
352 end
353 project_tree(projects) do |project, level|
353 project_tree(projects) do |project, level|
354 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
354 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
355 tag_options = {:value => project.id}
355 tag_options = {:value => project.id}
356 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
356 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
357 tag_options[:selected] = 'selected'
357 tag_options[:selected] = 'selected'
358 else
358 else
359 tag_options[:selected] = nil
359 tag_options[:selected] = nil
360 end
360 end
361 tag_options.merge!(yield(project)) if block_given?
361 tag_options.merge!(yield(project)) if block_given?
362 s << content_tag('option', name_prefix + h(project), tag_options)
362 s << content_tag('option', name_prefix + h(project), tag_options)
363 end
363 end
364 s.html_safe
364 s.html_safe
365 end
365 end
366
366
367 # Yields the given block for each project with its level in the tree
367 # Yields the given block for each project with its level in the tree
368 #
368 #
369 # Wrapper for Project#project_tree
369 # Wrapper for Project#project_tree
370 def project_tree(projects, &block)
370 def project_tree(projects, &block)
371 Project.project_tree(projects, &block)
371 Project.project_tree(projects, &block)
372 end
372 end
373
373
374 def principals_check_box_tags(name, principals)
374 def principals_check_box_tags(name, principals)
375 s = ''
375 s = ''
376 principals.each do |principal|
376 principals.each do |principal|
377 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
377 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
378 end
378 end
379 s.html_safe
379 s.html_safe
380 end
380 end
381
381
382 # Returns a string for users/groups option tags
382 # Returns a string for users/groups option tags
383 def principals_options_for_select(collection, selected=nil)
383 def principals_options_for_select(collection, selected=nil)
384 s = ''
384 s = ''
385 if collection.include?(User.current)
385 if collection.include?(User.current)
386 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
386 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
387 end
387 end
388 groups = ''
388 groups = ''
389 collection.sort.each do |element|
389 collection.sort.each do |element|
390 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
390 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
391 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
391 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
392 end
392 end
393 unless groups.empty?
393 unless groups.empty?
394 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
394 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
395 end
395 end
396 s.html_safe
396 s.html_safe
397 end
397 end
398
398
399 def option_tag(name, text, value, selected=nil, options={})
399 def option_tag(name, text, value, selected=nil, options={})
400 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
400 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
401 end
401 end
402
402
403 def truncate_single_line_raw(string, length)
403 def truncate_single_line_raw(string, length)
404 string.to_s.truncate(length).gsub(%r{[\r\n]+}m, ' ')
404 string.to_s.truncate(length).gsub(%r{[\r\n]+}m, ' ')
405 end
405 end
406
406
407 # Truncates at line break after 250 characters or options[:length]
407 # Truncates at line break after 250 characters or options[:length]
408 def truncate_lines(string, options={})
408 def truncate_lines(string, options={})
409 length = options[:length] || 250
409 length = options[:length] || 250
410 if string.to_s =~ /\A(.{#{length}}.*?)$/m
410 if string.to_s =~ /\A(.{#{length}}.*?)$/m
411 "#{$1}..."
411 "#{$1}..."
412 else
412 else
413 string
413 string
414 end
414 end
415 end
415 end
416
416
417 def anchor(text)
417 def anchor(text)
418 text.to_s.gsub(' ', '_')
418 text.to_s.gsub(' ', '_')
419 end
419 end
420
420
421 def html_hours(text)
421 def html_hours(text)
422 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
422 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
423 end
423 end
424
424
425 def authoring(created, author, options={})
425 def authoring(created, author, options={})
426 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
426 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
427 end
427 end
428
428
429 def time_tag(time)
429 def time_tag(time)
430 text = distance_of_time_in_words(Time.now, time)
430 text = distance_of_time_in_words(Time.now, time)
431 if @project
431 if @project
432 link_to(text, project_activity_path(@project, :from => User.current.time_to_date(time)), :title => format_time(time))
432 link_to(text, project_activity_path(@project, :from => User.current.time_to_date(time)), :title => format_time(time))
433 else
433 else
434 content_tag('abbr', text, :title => format_time(time))
434 content_tag('abbr', text, :title => format_time(time))
435 end
435 end
436 end
436 end
437
437
438 def syntax_highlight_lines(name, content)
438 def syntax_highlight_lines(name, content)
439 lines = []
439 lines = []
440 syntax_highlight(name, content).each_line { |line| lines << line }
440 syntax_highlight(name, content).each_line { |line| lines << line }
441 lines
441 lines
442 end
442 end
443
443
444 def syntax_highlight(name, content)
444 def syntax_highlight(name, content)
445 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
445 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
446 end
446 end
447
447
448 def to_path_param(path)
448 def to_path_param(path)
449 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
449 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
450 str.blank? ? nil : str
450 str.blank? ? nil : str
451 end
451 end
452
452
453 def reorder_links(name, url, method = :post)
453 def reorder_links(name, url, method = :post)
454 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
454 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
455 url.merge({"#{name}[move_to]" => 'highest'}),
455 url.merge({"#{name}[move_to]" => 'highest'}),
456 :method => method, :title => l(:label_sort_highest)) +
456 :method => method, :title => l(:label_sort_highest)) +
457 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
457 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
458 url.merge({"#{name}[move_to]" => 'higher'}),
458 url.merge({"#{name}[move_to]" => 'higher'}),
459 :method => method, :title => l(:label_sort_higher)) +
459 :method => method, :title => l(:label_sort_higher)) +
460 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
460 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
461 url.merge({"#{name}[move_to]" => 'lower'}),
461 url.merge({"#{name}[move_to]" => 'lower'}),
462 :method => method, :title => l(:label_sort_lower)) +
462 :method => method, :title => l(:label_sort_lower)) +
463 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
463 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
464 url.merge({"#{name}[move_to]" => 'lowest'}),
464 url.merge({"#{name}[move_to]" => 'lowest'}),
465 :method => method, :title => l(:label_sort_lowest))
465 :method => method, :title => l(:label_sort_lowest))
466 end
466 end
467
467
468 def breadcrumb(*args)
468 def breadcrumb(*args)
469 elements = args.flatten
469 elements = args.flatten
470 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
470 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
471 end
471 end
472
472
473 def other_formats_links(&block)
473 def other_formats_links(&block)
474 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
474 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
475 yield Redmine::Views::OtherFormatsBuilder.new(self)
475 yield Redmine::Views::OtherFormatsBuilder.new(self)
476 concat('</p>'.html_safe)
476 concat('</p>'.html_safe)
477 end
477 end
478
478
479 def page_header_title
479 def page_header_title
480 if @project.nil? || @project.new_record?
480 if @project.nil? || @project.new_record?
481 h(Setting.app_title)
481 h(Setting.app_title)
482 else
482 else
483 b = []
483 b = []
484 ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
484 ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
485 if ancestors.any?
485 if ancestors.any?
486 root = ancestors.shift
486 root = ancestors.shift
487 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
487 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
488 if ancestors.size > 2
488 if ancestors.size > 2
489 b << "\xe2\x80\xa6"
489 b << "\xe2\x80\xa6"
490 ancestors = ancestors[-2, 2]
490 ancestors = ancestors[-2, 2]
491 end
491 end
492 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
492 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
493 end
493 end
494 b << h(@project)
494 b << h(@project)
495 b.join(" \xc2\xbb ").html_safe
495 b.join(" \xc2\xbb ").html_safe
496 end
496 end
497 end
497 end
498
498
499 # Returns a h2 tag and sets the html title with the given arguments
499 # Returns a h2 tag and sets the html title with the given arguments
500 def title(*args)
500 def title(*args)
501 strings = args.map do |arg|
501 strings = args.map do |arg|
502 if arg.is_a?(Array) && arg.size >= 2
502 if arg.is_a?(Array) && arg.size >= 2
503 link_to(*arg)
503 link_to(*arg)
504 else
504 else
505 h(arg.to_s)
505 h(arg.to_s)
506 end
506 end
507 end
507 end
508 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
508 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
509 content_tag('h2', strings.join(' &#187; ').html_safe)
509 content_tag('h2', strings.join(' &#187; ').html_safe)
510 end
510 end
511
511
512 # Sets the html title
512 # Sets the html title
513 # Returns the html title when called without arguments
513 # Returns the html title when called without arguments
514 # Current project name and app_title and automatically appended
514 # Current project name and app_title and automatically appended
515 # Exemples:
515 # Exemples:
516 # html_title 'Foo', 'Bar'
516 # html_title 'Foo', 'Bar'
517 # html_title # => 'Foo - Bar - My Project - Redmine'
517 # html_title # => 'Foo - Bar - My Project - Redmine'
518 def html_title(*args)
518 def html_title(*args)
519 if args.empty?
519 if args.empty?
520 title = @html_title || []
520 title = @html_title || []
521 title << @project.name if @project
521 title << @project.name if @project
522 title << Setting.app_title unless Setting.app_title == title.last
522 title << Setting.app_title unless Setting.app_title == title.last
523 title.reject(&:blank?).join(' - ')
523 title.reject(&:blank?).join(' - ')
524 else
524 else
525 @html_title ||= []
525 @html_title ||= []
526 @html_title += args
526 @html_title += args
527 end
527 end
528 end
528 end
529
529
530 # Returns the theme, controller name, and action as css classes for the
530 # Returns the theme, controller name, and action as css classes for the
531 # HTML body.
531 # HTML body.
532 def body_css_classes
532 def body_css_classes
533 css = []
533 css = []
534 if theme = Redmine::Themes.theme(Setting.ui_theme)
534 if theme = Redmine::Themes.theme(Setting.ui_theme)
535 css << 'theme-' + theme.name
535 css << 'theme-' + theme.name
536 end
536 end
537
537
538 css << 'project-' + @project.identifier if @project && @project.identifier.present?
538 css << 'project-' + @project.identifier if @project && @project.identifier.present?
539 css << 'controller-' + controller_name
539 css << 'controller-' + controller_name
540 css << 'action-' + action_name
540 css << 'action-' + action_name
541 css.join(' ')
541 css.join(' ')
542 end
542 end
543
543
544 def accesskey(s)
544 def accesskey(s)
545 @used_accesskeys ||= []
545 @used_accesskeys ||= []
546 key = Redmine::AccessKeys.key_for(s)
546 key = Redmine::AccessKeys.key_for(s)
547 return nil if @used_accesskeys.include?(key)
547 return nil if @used_accesskeys.include?(key)
548 @used_accesskeys << key
548 @used_accesskeys << key
549 key
549 key
550 end
550 end
551
551
552 # Formats text according to system settings.
552 # Formats text according to system settings.
553 # 2 ways to call this method:
553 # 2 ways to call this method:
554 # * with a String: textilizable(text, options)
554 # * with a String: textilizable(text, options)
555 # * with an object and one of its attribute: textilizable(issue, :description, options)
555 # * with an object and one of its attribute: textilizable(issue, :description, options)
556 def textilizable(*args)
556 def textilizable(*args)
557 options = args.last.is_a?(Hash) ? args.pop : {}
557 options = args.last.is_a?(Hash) ? args.pop : {}
558 case args.size
558 case args.size
559 when 1
559 when 1
560 obj = options[:object]
560 obj = options[:object]
561 text = args.shift
561 text = args.shift
562 when 2
562 when 2
563 obj = args.shift
563 obj = args.shift
564 attr = args.shift
564 attr = args.shift
565 text = obj.send(attr).to_s
565 text = obj.send(attr).to_s
566 else
566 else
567 raise ArgumentError, 'invalid arguments to textilizable'
567 raise ArgumentError, 'invalid arguments to textilizable'
568 end
568 end
569 return '' if text.blank?
569 return '' if text.blank?
570 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
570 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
571 @only_path = only_path = options.delete(:only_path) == false ? false : true
571 @only_path = only_path = options.delete(:only_path) == false ? false : true
572
572
573 text = text.dup
573 text = text.dup
574 macros = catch_macros(text)
574 macros = catch_macros(text)
575 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
575 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
576
576
577 @parsed_headings = []
577 @parsed_headings = []
578 @heading_anchors = {}
578 @heading_anchors = {}
579 @current_section = 0 if options[:edit_section_links]
579 @current_section = 0 if options[:edit_section_links]
580
580
581 parse_sections(text, project, obj, attr, only_path, options)
581 parse_sections(text, project, obj, attr, only_path, options)
582 text = parse_non_pre_blocks(text, obj, macros) do |text|
582 text = parse_non_pre_blocks(text, obj, macros) do |text|
583 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
583 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
584 send method_name, text, project, obj, attr, only_path, options
584 send method_name, text, project, obj, attr, only_path, options
585 end
585 end
586 end
586 end
587 parse_headings(text, project, obj, attr, only_path, options)
587 parse_headings(text, project, obj, attr, only_path, options)
588
588
589 if @parsed_headings.any?
589 if @parsed_headings.any?
590 replace_toc(text, @parsed_headings)
590 replace_toc(text, @parsed_headings)
591 end
591 end
592
592
593 text.html_safe
593 text.html_safe
594 end
594 end
595
595
596 def parse_non_pre_blocks(text, obj, macros)
596 def parse_non_pre_blocks(text, obj, macros)
597 s = StringScanner.new(text)
597 s = StringScanner.new(text)
598 tags = []
598 tags = []
599 parsed = ''
599 parsed = ''
600 while !s.eos?
600 while !s.eos?
601 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
601 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
602 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
602 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
603 if tags.empty?
603 if tags.empty?
604 yield text
604 yield text
605 inject_macros(text, obj, macros) if macros.any?
605 inject_macros(text, obj, macros) if macros.any?
606 else
606 else
607 inject_macros(text, obj, macros, false) if macros.any?
607 inject_macros(text, obj, macros, false) if macros.any?
608 end
608 end
609 parsed << text
609 parsed << text
610 if tag
610 if tag
611 if closing
611 if closing
612 if tags.last.casecmp(tag) == 0
612 if tags.last.casecmp(tag) == 0
613 tags.pop
613 tags.pop
614 end
614 end
615 else
615 else
616 tags << tag.downcase
616 tags << tag.downcase
617 end
617 end
618 parsed << full_tag
618 parsed << full_tag
619 end
619 end
620 end
620 end
621 # Close any non closing tags
621 # Close any non closing tags
622 while tag = tags.pop
622 while tag = tags.pop
623 parsed << "</#{tag}>"
623 parsed << "</#{tag}>"
624 end
624 end
625 parsed
625 parsed
626 end
626 end
627
627
628 def parse_inline_attachments(text, project, obj, attr, only_path, options)
628 def parse_inline_attachments(text, project, obj, attr, only_path, options)
629 return if options[:inline_attachments] == false
629 return if options[:inline_attachments] == false
630
630
631 # when using an image link, try to use an attachment, if possible
631 # when using an image link, try to use an attachment, if possible
632 attachments = options[:attachments] || []
632 attachments = options[:attachments] || []
633 attachments += obj.attachments if obj.respond_to?(:attachments)
633 attachments += obj.attachments if obj.respond_to?(:attachments)
634 if attachments.present?
634 if attachments.present?
635 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
635 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
636 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
636 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
637 # search for the picture in attachments
637 # search for the picture in attachments
638 if found = Attachment.latest_attach(attachments, CGI.unescape(filename))
638 if found = Attachment.latest_attach(attachments, CGI.unescape(filename))
639 image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
639 image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
640 desc = found.description.to_s.gsub('"', '')
640 desc = found.description.to_s.gsub('"', '')
641 if !desc.blank? && alttext.blank?
641 if !desc.blank? && alttext.blank?
642 alt = " title=\"#{desc}\" alt=\"#{desc}\""
642 alt = " title=\"#{desc}\" alt=\"#{desc}\""
643 end
643 end
644 "src=\"#{image_url}\"#{alt}"
644 "src=\"#{image_url}\"#{alt}"
645 else
645 else
646 m
646 m
647 end
647 end
648 end
648 end
649 end
649 end
650 end
650 end
651
651
652 # Wiki links
652 # Wiki links
653 #
653 #
654 # Examples:
654 # Examples:
655 # [[mypage]]
655 # [[mypage]]
656 # [[mypage|mytext]]
656 # [[mypage|mytext]]
657 # wiki links can refer other project wikis, using project name or identifier:
657 # wiki links can refer other project wikis, using project name or identifier:
658 # [[project:]] -> wiki starting page
658 # [[project:]] -> wiki starting page
659 # [[project:|mytext]]
659 # [[project:|mytext]]
660 # [[project:mypage]]
660 # [[project:mypage]]
661 # [[project:mypage|mytext]]
661 # [[project:mypage|mytext]]
662 def parse_wiki_links(text, project, obj, attr, only_path, options)
662 def parse_wiki_links(text, project, obj, attr, only_path, options)
663 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
663 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
664 link_project = project
664 link_project = project
665 esc, all, page, title = $1, $2, $3, $5
665 esc, all, page, title = $1, $2, $3, $5
666 if esc.nil?
666 if esc.nil?
667 if page =~ /^([^\:]+)\:(.*)$/
667 if page =~ /^([^\:]+)\:(.*)$/
668 identifier, page = $1, $2
668 identifier, page = $1, $2
669 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
669 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
670 title ||= identifier if page.blank?
670 title ||= identifier if page.blank?
671 end
671 end
672
672
673 if link_project && link_project.wiki
673 if link_project && link_project.wiki
674 # extract anchor
674 # extract anchor
675 anchor = nil
675 anchor = nil
676 if page =~ /^(.+?)\#(.+)$/
676 if page =~ /^(.+?)\#(.+)$/
677 page, anchor = $1, $2
677 page, anchor = $1, $2
678 end
678 end
679 anchor = sanitize_anchor_name(anchor) if anchor.present?
679 anchor = sanitize_anchor_name(anchor) if anchor.present?
680 # check if page exists
680 # check if page exists
681 wiki_page = link_project.wiki.find_page(page)
681 wiki_page = link_project.wiki.find_page(page)
682 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
682 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
683 "##{anchor}"
683 "##{anchor}"
684 else
684 else
685 case options[:wiki_links]
685 case options[:wiki_links]
686 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
686 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
687 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
687 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
688 else
688 else
689 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
689 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
690 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
690 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
691 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
691 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
692 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
692 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
693 end
693 end
694 end
694 end
695 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
695 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
696 else
696 else
697 # project or wiki doesn't exist
697 # project or wiki doesn't exist
698 all
698 all
699 end
699 end
700 else
700 else
701 all
701 all
702 end
702 end
703 end
703 end
704 end
704 end
705
705
706 # Redmine links
706 # Redmine links
707 #
707 #
708 # Examples:
708 # Examples:
709 # Issues:
709 # Issues:
710 # #52 -> Link to issue #52
710 # #52 -> Link to issue #52
711 # Changesets:
711 # Changesets:
712 # r52 -> Link to revision 52
712 # r52 -> Link to revision 52
713 # commit:a85130f -> Link to scmid starting with a85130f
713 # commit:a85130f -> Link to scmid starting with a85130f
714 # Documents:
714 # Documents:
715 # document#17 -> Link to document with id 17
715 # document#17 -> Link to document with id 17
716 # document:Greetings -> Link to the document with title "Greetings"
716 # document:Greetings -> Link to the document with title "Greetings"
717 # document:"Some document" -> Link to the document with title "Some document"
717 # document:"Some document" -> Link to the document with title "Some document"
718 # Versions:
718 # Versions:
719 # version#3 -> Link to version with id 3
719 # version#3 -> Link to version with id 3
720 # version:1.0.0 -> Link to version named "1.0.0"
720 # version:1.0.0 -> Link to version named "1.0.0"
721 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
721 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
722 # Attachments:
722 # Attachments:
723 # attachment:file.zip -> Link to the attachment of the current object named file.zip
723 # attachment:file.zip -> Link to the attachment of the current object named file.zip
724 # Source files:
724 # Source files:
725 # source:some/file -> Link to the file located at /some/file in the project's repository
725 # source:some/file -> Link to the file located at /some/file in the project's repository
726 # source:some/file@52 -> Link to the file's revision 52
726 # source:some/file@52 -> Link to the file's revision 52
727 # source:some/file#L120 -> Link to line 120 of the file
727 # source:some/file#L120 -> Link to line 120 of the file
728 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
728 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
729 # export:some/file -> Force the download of the file
729 # export:some/file -> Force the download of the file
730 # Forum messages:
730 # Forum messages:
731 # message#1218 -> Link to message with id 1218
731 # message#1218 -> Link to message with id 1218
732 # Projects:
732 # Projects:
733 # project:someproject -> Link to project named "someproject"
733 # project:someproject -> Link to project named "someproject"
734 # project#3 -> Link to project with id 3
734 # project#3 -> Link to project with id 3
735 #
735 #
736 # Links can refer other objects from other projects, using project identifier:
736 # Links can refer other objects from other projects, using project identifier:
737 # identifier:r52
737 # identifier:r52
738 # identifier:document:"Some document"
738 # identifier:document:"Some document"
739 # identifier:version:1.0.0
739 # identifier:version:1.0.0
740 # identifier:source:some/file
740 # identifier:source:some/file
741 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
741 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
742 text.gsub!(%r{<a( [^>]+?)?>(.*?)</a>|([\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|
742 text.gsub!(%r{<a( [^>]+?)?>(.*?)</a>|([\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|
743 tag_content, leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $3, $4, $5, $6, $7, $12, $13, $10 || $14 || $20, $16 || $21, $17, $19
743 tag_content, leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $3, $4, $5, $6, $7, $12, $13, $10 || $14 || $20, $16 || $21, $17, $19
744 if tag_content
744 if tag_content
745 $&
745 $&
746 else
746 else
747 link = nil
747 link = nil
748 project = default_project
748 project = default_project
749 if project_identifier
749 if project_identifier
750 project = Project.visible.find_by_identifier(project_identifier)
750 project = Project.visible.find_by_identifier(project_identifier)
751 end
751 end
752 if esc.nil?
752 if esc.nil?
753 if prefix.nil? && sep == 'r'
753 if prefix.nil? && sep == 'r'
754 if project
754 if project
755 repository = nil
755 repository = nil
756 if repo_identifier
756 if repo_identifier
757 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
757 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
758 else
758 else
759 repository = project.repository
759 repository = project.repository
760 end
760 end
761 # project.changesets.visible raises an SQL error because of a double join on repositories
761 # project.changesets.visible raises an SQL error because of a double join on repositories
762 if repository &&
762 if repository &&
763 (changeset = Changeset.visible.
763 (changeset = Changeset.visible.
764 find_by_repository_id_and_revision(repository.id, identifier))
764 find_by_repository_id_and_revision(repository.id, identifier))
765 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
765 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
766 {:only_path => only_path, :controller => 'repositories',
766 {:only_path => only_path, :controller => 'repositories',
767 :action => 'revision', :id => project,
767 :action => 'revision', :id => project,
768 :repository_id => repository.identifier_param,
768 :repository_id => repository.identifier_param,
769 :rev => changeset.revision},
769 :rev => changeset.revision},
770 :class => 'changeset',
770 :class => 'changeset',
771 :title => truncate_single_line_raw(changeset.comments, 100))
771 :title => truncate_single_line_raw(changeset.comments, 100))
772 end
772 end
773 end
773 end
774 elsif sep == '#'
774 elsif sep == '#'
775 oid = identifier.to_i
775 oid = identifier.to_i
776 case prefix
776 case prefix
777 when nil
777 when nil
778 if oid.to_s == identifier &&
778 if oid.to_s == identifier &&
779 issue = Issue.visible.find_by_id(oid)
779 issue = Issue.visible.find_by_id(oid)
780 anchor = comment_id ? "note-#{comment_id}" : nil
780 anchor = comment_id ? "note-#{comment_id}" : nil
781 link = link_to("##{oid}#{comment_suffix}",
781 link = link_to("##{oid}#{comment_suffix}",
782 issue_url(issue, :only_path => only_path, :anchor => anchor),
782 issue_url(issue, :only_path => only_path, :anchor => anchor),
783 :class => issue.css_classes,
783 :class => issue.css_classes,
784 :title => "#{issue.subject.truncate(100)} (#{issue.status.name})")
784 :title => "#{issue.tracker.name}: #{issue.subject.truncate(100)} (#{issue.status.name})")
785 end
785 end
786 when 'document'
786 when 'document'
787 if document = Document.visible.find_by_id(oid)
787 if document = Document.visible.find_by_id(oid)
788 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
788 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
789 end
789 end
790 when 'version'
790 when 'version'
791 if version = Version.visible.find_by_id(oid)
791 if version = Version.visible.find_by_id(oid)
792 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
792 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
793 end
793 end
794 when 'message'
794 when 'message'
795 if message = Message.visible.find_by_id(oid)
795 if message = Message.visible.find_by_id(oid)
796 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
796 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
797 end
797 end
798 when 'forum'
798 when 'forum'
799 if board = Board.visible.find_by_id(oid)
799 if board = Board.visible.find_by_id(oid)
800 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
800 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
801 end
801 end
802 when 'news'
802 when 'news'
803 if news = News.visible.find_by_id(oid)
803 if news = News.visible.find_by_id(oid)
804 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
804 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
805 end
805 end
806 when 'project'
806 when 'project'
807 if p = Project.visible.find_by_id(oid)
807 if p = Project.visible.find_by_id(oid)
808 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
808 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
809 end
809 end
810 end
810 end
811 elsif sep == ':'
811 elsif sep == ':'
812 # removes the double quotes if any
812 # removes the double quotes if any
813 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
813 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
814 name = CGI.unescapeHTML(name)
814 name = CGI.unescapeHTML(name)
815 case prefix
815 case prefix
816 when 'document'
816 when 'document'
817 if project && document = project.documents.visible.find_by_title(name)
817 if project && document = project.documents.visible.find_by_title(name)
818 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
818 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
819 end
819 end
820 when 'version'
820 when 'version'
821 if project && version = project.versions.visible.find_by_name(name)
821 if project && version = project.versions.visible.find_by_name(name)
822 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
822 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
823 end
823 end
824 when 'forum'
824 when 'forum'
825 if project && board = project.boards.visible.find_by_name(name)
825 if project && board = project.boards.visible.find_by_name(name)
826 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
826 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
827 end
827 end
828 when 'news'
828 when 'news'
829 if project && news = project.news.visible.find_by_title(name)
829 if project && news = project.news.visible.find_by_title(name)
830 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
830 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
831 end
831 end
832 when 'commit', 'source', 'export'
832 when 'commit', 'source', 'export'
833 if project
833 if project
834 repository = nil
834 repository = nil
835 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
835 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
836 repo_prefix, repo_identifier, name = $1, $2, $3
836 repo_prefix, repo_identifier, name = $1, $2, $3
837 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
837 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
838 else
838 else
839 repository = project.repository
839 repository = project.repository
840 end
840 end
841 if prefix == 'commit'
841 if prefix == 'commit'
842 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
842 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
843 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},
843 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},
844 :class => 'changeset',
844 :class => 'changeset',
845 :title => truncate_single_line_raw(changeset.comments, 100)
845 :title => truncate_single_line_raw(changeset.comments, 100)
846 end
846 end
847 else
847 else
848 if repository && User.current.allowed_to?(:browse_repository, project)
848 if repository && User.current.allowed_to?(:browse_repository, project)
849 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
849 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
850 path, rev, anchor = $1, $3, $5
850 path, rev, anchor = $1, $3, $5
851 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
851 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
852 :path => to_path_param(path),
852 :path => to_path_param(path),
853 :rev => rev,
853 :rev => rev,
854 :anchor => anchor},
854 :anchor => anchor},
855 :class => (prefix == 'export' ? 'source download' : 'source')
855 :class => (prefix == 'export' ? 'source download' : 'source')
856 end
856 end
857 end
857 end
858 repo_prefix = nil
858 repo_prefix = nil
859 end
859 end
860 when 'attachment'
860 when 'attachment'
861 attachments = options[:attachments] || []
861 attachments = options[:attachments] || []
862 attachments += obj.attachments if obj.respond_to?(:attachments)
862 attachments += obj.attachments if obj.respond_to?(:attachments)
863 if attachments && attachment = Attachment.latest_attach(attachments, name)
863 if attachments && attachment = Attachment.latest_attach(attachments, name)
864 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
864 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
865 end
865 end
866 when 'project'
866 when 'project'
867 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
867 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
868 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
868 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
869 end
869 end
870 end
870 end
871 end
871 end
872 end
872 end
873 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
873 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
874 end
874 end
875 end
875 end
876 end
876 end
877
877
878 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
878 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
879
879
880 def parse_sections(text, project, obj, attr, only_path, options)
880 def parse_sections(text, project, obj, attr, only_path, options)
881 return unless options[:edit_section_links]
881 return unless options[:edit_section_links]
882 text.gsub!(HEADING_RE) do
882 text.gsub!(HEADING_RE) do
883 heading = $1
883 heading = $1
884 @current_section += 1
884 @current_section += 1
885 if @current_section > 1
885 if @current_section > 1
886 content_tag('div',
886 content_tag('div',
887 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
887 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
888 :class => 'contextual',
888 :class => 'contextual',
889 :title => l(:button_edit_section),
889 :title => l(:button_edit_section),
890 :id => "section-#{@current_section}") + heading.html_safe
890 :id => "section-#{@current_section}") + heading.html_safe
891 else
891 else
892 heading
892 heading
893 end
893 end
894 end
894 end
895 end
895 end
896
896
897 # Headings and TOC
897 # Headings and TOC
898 # Adds ids and links to headings unless options[:headings] is set to false
898 # Adds ids and links to headings unless options[:headings] is set to false
899 def parse_headings(text, project, obj, attr, only_path, options)
899 def parse_headings(text, project, obj, attr, only_path, options)
900 return if options[:headings] == false
900 return if options[:headings] == false
901
901
902 text.gsub!(HEADING_RE) do
902 text.gsub!(HEADING_RE) do
903 level, attrs, content = $2.to_i, $3, $4
903 level, attrs, content = $2.to_i, $3, $4
904 item = strip_tags(content).strip
904 item = strip_tags(content).strip
905 anchor = sanitize_anchor_name(item)
905 anchor = sanitize_anchor_name(item)
906 # used for single-file wiki export
906 # used for single-file wiki export
907 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
907 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
908 @heading_anchors[anchor] ||= 0
908 @heading_anchors[anchor] ||= 0
909 idx = (@heading_anchors[anchor] += 1)
909 idx = (@heading_anchors[anchor] += 1)
910 if idx > 1
910 if idx > 1
911 anchor = "#{anchor}-#{idx}"
911 anchor = "#{anchor}-#{idx}"
912 end
912 end
913 @parsed_headings << [level, anchor, item]
913 @parsed_headings << [level, anchor, item]
914 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
914 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
915 end
915 end
916 end
916 end
917
917
918 MACROS_RE = /(
918 MACROS_RE = /(
919 (!)? # escaping
919 (!)? # escaping
920 (
920 (
921 \{\{ # opening tag
921 \{\{ # opening tag
922 ([\w]+) # macro name
922 ([\w]+) # macro name
923 (\(([^\n\r]*?)\))? # optional arguments
923 (\(([^\n\r]*?)\))? # optional arguments
924 ([\n\r].*?[\n\r])? # optional block of text
924 ([\n\r].*?[\n\r])? # optional block of text
925 \}\} # closing tag
925 \}\} # closing tag
926 )
926 )
927 )/mx unless const_defined?(:MACROS_RE)
927 )/mx unless const_defined?(:MACROS_RE)
928
928
929 MACRO_SUB_RE = /(
929 MACRO_SUB_RE = /(
930 \{\{
930 \{\{
931 macro\((\d+)\)
931 macro\((\d+)\)
932 \}\}
932 \}\}
933 )/x unless const_defined?(:MACRO_SUB_RE)
933 )/x unless const_defined?(:MACRO_SUB_RE)
934
934
935 # Extracts macros from text
935 # Extracts macros from text
936 def catch_macros(text)
936 def catch_macros(text)
937 macros = {}
937 macros = {}
938 text.gsub!(MACROS_RE) do
938 text.gsub!(MACROS_RE) do
939 all, macro = $1, $4.downcase
939 all, macro = $1, $4.downcase
940 if macro_exists?(macro) || all =~ MACRO_SUB_RE
940 if macro_exists?(macro) || all =~ MACRO_SUB_RE
941 index = macros.size
941 index = macros.size
942 macros[index] = all
942 macros[index] = all
943 "{{macro(#{index})}}"
943 "{{macro(#{index})}}"
944 else
944 else
945 all
945 all
946 end
946 end
947 end
947 end
948 macros
948 macros
949 end
949 end
950
950
951 # Executes and replaces macros in text
951 # Executes and replaces macros in text
952 def inject_macros(text, obj, macros, execute=true)
952 def inject_macros(text, obj, macros, execute=true)
953 text.gsub!(MACRO_SUB_RE) do
953 text.gsub!(MACRO_SUB_RE) do
954 all, index = $1, $2.to_i
954 all, index = $1, $2.to_i
955 orig = macros.delete(index)
955 orig = macros.delete(index)
956 if execute && orig && orig =~ MACROS_RE
956 if execute && orig && orig =~ MACROS_RE
957 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
957 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
958 if esc.nil?
958 if esc.nil?
959 h(exec_macro(macro, obj, args, block) || all)
959 h(exec_macro(macro, obj, args, block) || all)
960 else
960 else
961 h(all)
961 h(all)
962 end
962 end
963 elsif orig
963 elsif orig
964 h(orig)
964 h(orig)
965 else
965 else
966 h(all)
966 h(all)
967 end
967 end
968 end
968 end
969 end
969 end
970
970
971 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
971 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
972
972
973 # Renders the TOC with given headings
973 # Renders the TOC with given headings
974 def replace_toc(text, headings)
974 def replace_toc(text, headings)
975 text.gsub!(TOC_RE) do
975 text.gsub!(TOC_RE) do
976 left_align, right_align = $2, $3
976 left_align, right_align = $2, $3
977 # Keep only the 4 first levels
977 # Keep only the 4 first levels
978 headings = headings.select{|level, anchor, item| level <= 4}
978 headings = headings.select{|level, anchor, item| level <= 4}
979 if headings.empty?
979 if headings.empty?
980 ''
980 ''
981 else
981 else
982 div_class = 'toc'
982 div_class = 'toc'
983 div_class << ' right' if right_align
983 div_class << ' right' if right_align
984 div_class << ' left' if left_align
984 div_class << ' left' if left_align
985 out = "<ul class=\"#{div_class}\"><li>"
985 out = "<ul class=\"#{div_class}\"><li>"
986 root = headings.map(&:first).min
986 root = headings.map(&:first).min
987 current = root
987 current = root
988 started = false
988 started = false
989 headings.each do |level, anchor, item|
989 headings.each do |level, anchor, item|
990 if level > current
990 if level > current
991 out << '<ul><li>' * (level - current)
991 out << '<ul><li>' * (level - current)
992 elsif level < current
992 elsif level < current
993 out << "</li></ul>\n" * (current - level) + "</li><li>"
993 out << "</li></ul>\n" * (current - level) + "</li><li>"
994 elsif started
994 elsif started
995 out << '</li><li>'
995 out << '</li><li>'
996 end
996 end
997 out << "<a href=\"##{anchor}\">#{item}</a>"
997 out << "<a href=\"##{anchor}\">#{item}</a>"
998 current = level
998 current = level
999 started = true
999 started = true
1000 end
1000 end
1001 out << '</li></ul>' * (current - root)
1001 out << '</li></ul>' * (current - root)
1002 out << '</li></ul>'
1002 out << '</li></ul>'
1003 end
1003 end
1004 end
1004 end
1005 end
1005 end
1006
1006
1007 # Same as Rails' simple_format helper without using paragraphs
1007 # Same as Rails' simple_format helper without using paragraphs
1008 def simple_format_without_paragraph(text)
1008 def simple_format_without_paragraph(text)
1009 text.to_s.
1009 text.to_s.
1010 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1010 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1011 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1011 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1012 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1012 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1013 html_safe
1013 html_safe
1014 end
1014 end
1015
1015
1016 def lang_options_for_select(blank=true)
1016 def lang_options_for_select(blank=true)
1017 (blank ? [["(auto)", ""]] : []) + languages_options
1017 (blank ? [["(auto)", ""]] : []) + languages_options
1018 end
1018 end
1019
1019
1020 def labelled_form_for(*args, &proc)
1020 def labelled_form_for(*args, &proc)
1021 args << {} unless args.last.is_a?(Hash)
1021 args << {} unless args.last.is_a?(Hash)
1022 options = args.last
1022 options = args.last
1023 if args.first.is_a?(Symbol)
1023 if args.first.is_a?(Symbol)
1024 options.merge!(:as => args.shift)
1024 options.merge!(:as => args.shift)
1025 end
1025 end
1026 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1026 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1027 form_for(*args, &proc)
1027 form_for(*args, &proc)
1028 end
1028 end
1029
1029
1030 def labelled_fields_for(*args, &proc)
1030 def labelled_fields_for(*args, &proc)
1031 args << {} unless args.last.is_a?(Hash)
1031 args << {} unless args.last.is_a?(Hash)
1032 options = args.last
1032 options = args.last
1033 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1033 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1034 fields_for(*args, &proc)
1034 fields_for(*args, &proc)
1035 end
1035 end
1036
1036
1037 def error_messages_for(*objects)
1037 def error_messages_for(*objects)
1038 html = ""
1038 html = ""
1039 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1039 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1040 errors = objects.map {|o| o.errors.full_messages}.flatten
1040 errors = objects.map {|o| o.errors.full_messages}.flatten
1041 if errors.any?
1041 if errors.any?
1042 html << "<div id='errorExplanation'><ul>\n"
1042 html << "<div id='errorExplanation'><ul>\n"
1043 errors.each do |error|
1043 errors.each do |error|
1044 html << "<li>#{h error}</li>\n"
1044 html << "<li>#{h error}</li>\n"
1045 end
1045 end
1046 html << "</ul></div>\n"
1046 html << "</ul></div>\n"
1047 end
1047 end
1048 html.html_safe
1048 html.html_safe
1049 end
1049 end
1050
1050
1051 def delete_link(url, options={})
1051 def delete_link(url, options={})
1052 options = {
1052 options = {
1053 :method => :delete,
1053 :method => :delete,
1054 :data => {:confirm => l(:text_are_you_sure)},
1054 :data => {:confirm => l(:text_are_you_sure)},
1055 :class => 'icon icon-del'
1055 :class => 'icon icon-del'
1056 }.merge(options)
1056 }.merge(options)
1057
1057
1058 link_to l(:button_delete), url, options
1058 link_to l(:button_delete), url, options
1059 end
1059 end
1060
1060
1061 def preview_link(url, form, target='preview', options={})
1061 def preview_link(url, form, target='preview', options={})
1062 content_tag 'a', l(:label_preview), {
1062 content_tag 'a', l(:label_preview), {
1063 :href => "#",
1063 :href => "#",
1064 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1064 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1065 :accesskey => accesskey(:preview)
1065 :accesskey => accesskey(:preview)
1066 }.merge(options)
1066 }.merge(options)
1067 end
1067 end
1068
1068
1069 def link_to_function(name, function, html_options={})
1069 def link_to_function(name, function, html_options={})
1070 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1070 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1071 end
1071 end
1072
1072
1073 # Helper to render JSON in views
1073 # Helper to render JSON in views
1074 def raw_json(arg)
1074 def raw_json(arg)
1075 arg.to_json.to_s.gsub('/', '\/').html_safe
1075 arg.to_json.to_s.gsub('/', '\/').html_safe
1076 end
1076 end
1077
1077
1078 def back_url
1078 def back_url
1079 url = params[:back_url]
1079 url = params[:back_url]
1080 if url.nil? && referer = request.env['HTTP_REFERER']
1080 if url.nil? && referer = request.env['HTTP_REFERER']
1081 url = CGI.unescape(referer.to_s)
1081 url = CGI.unescape(referer.to_s)
1082 end
1082 end
1083 url
1083 url
1084 end
1084 end
1085
1085
1086 def back_url_hidden_field_tag
1086 def back_url_hidden_field_tag
1087 url = back_url
1087 url = back_url
1088 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1088 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1089 end
1089 end
1090
1090
1091 def check_all_links(form_name)
1091 def check_all_links(form_name)
1092 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1092 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1093 " | ".html_safe +
1093 " | ".html_safe +
1094 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1094 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1095 end
1095 end
1096
1096
1097 def toggle_checkboxes_link(selector)
1097 def toggle_checkboxes_link(selector)
1098 link_to_function image_tag('toggle_check.png'),
1098 link_to_function image_tag('toggle_check.png'),
1099 "toggleCheckboxesBySelector('#{selector}')",
1099 "toggleCheckboxesBySelector('#{selector}')",
1100 :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}"
1100 :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}"
1101 end
1101 end
1102
1102
1103 def progress_bar(pcts, options={})
1103 def progress_bar(pcts, options={})
1104 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1104 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1105 pcts = pcts.collect(&:round)
1105 pcts = pcts.collect(&:round)
1106 pcts[1] = pcts[1] - pcts[0]
1106 pcts[1] = pcts[1] - pcts[0]
1107 pcts << (100 - pcts[1] - pcts[0])
1107 pcts << (100 - pcts[1] - pcts[0])
1108 width = options[:width] || '100px;'
1108 width = options[:width] || '100px;'
1109 legend = options[:legend] || ''
1109 legend = options[:legend] || ''
1110 content_tag('table',
1110 content_tag('table',
1111 content_tag('tr',
1111 content_tag('tr',
1112 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1112 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1113 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1113 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1114 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1114 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1115 ), :class => "progress progress-#{pcts[0]}", :style => "width: #{width};").html_safe +
1115 ), :class => "progress progress-#{pcts[0]}", :style => "width: #{width};").html_safe +
1116 content_tag('p', legend, :class => 'percent').html_safe
1116 content_tag('p', legend, :class => 'percent').html_safe
1117 end
1117 end
1118
1118
1119 def checked_image(checked=true)
1119 def checked_image(checked=true)
1120 if checked
1120 if checked
1121 @checked_image_tag ||= image_tag('toggle_check.png')
1121 @checked_image_tag ||= image_tag('toggle_check.png')
1122 end
1122 end
1123 end
1123 end
1124
1124
1125 def context_menu(url)
1125 def context_menu(url)
1126 unless @context_menu_included
1126 unless @context_menu_included
1127 content_for :header_tags do
1127 content_for :header_tags do
1128 javascript_include_tag('context_menu') +
1128 javascript_include_tag('context_menu') +
1129 stylesheet_link_tag('context_menu')
1129 stylesheet_link_tag('context_menu')
1130 end
1130 end
1131 if l(:direction) == 'rtl'
1131 if l(:direction) == 'rtl'
1132 content_for :header_tags do
1132 content_for :header_tags do
1133 stylesheet_link_tag('context_menu_rtl')
1133 stylesheet_link_tag('context_menu_rtl')
1134 end
1134 end
1135 end
1135 end
1136 @context_menu_included = true
1136 @context_menu_included = true
1137 end
1137 end
1138 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1138 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1139 end
1139 end
1140
1140
1141 def calendar_for(field_id)
1141 def calendar_for(field_id)
1142 include_calendar_headers_tags
1142 include_calendar_headers_tags
1143 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1143 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1144 end
1144 end
1145
1145
1146 def include_calendar_headers_tags
1146 def include_calendar_headers_tags
1147 unless @calendar_headers_tags_included
1147 unless @calendar_headers_tags_included
1148 tags = ''.html_safe
1148 tags = ''.html_safe
1149 @calendar_headers_tags_included = true
1149 @calendar_headers_tags_included = true
1150 content_for :header_tags do
1150 content_for :header_tags do
1151 start_of_week = Setting.start_of_week
1151 start_of_week = Setting.start_of_week
1152 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1152 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1153 # Redmine uses 1..7 (monday..sunday) in settings and locales
1153 # Redmine uses 1..7 (monday..sunday) in settings and locales
1154 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1154 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1155 start_of_week = start_of_week.to_i % 7
1155 start_of_week = start_of_week.to_i % 7
1156 tags << javascript_tag(
1156 tags << javascript_tag(
1157 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1157 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1158 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1158 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1159 path_to_image('/images/calendar.png') +
1159 path_to_image('/images/calendar.png') +
1160 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1160 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1161 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1161 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1162 "beforeShow: beforeShowDatePicker};")
1162 "beforeShow: beforeShowDatePicker};")
1163 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1163 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1164 unless jquery_locale == 'en'
1164 unless jquery_locale == 'en'
1165 tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
1165 tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
1166 end
1166 end
1167 tags
1167 tags
1168 end
1168 end
1169 end
1169 end
1170 end
1170 end
1171
1171
1172 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1172 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1173 # Examples:
1173 # Examples:
1174 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1174 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1175 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1175 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1176 #
1176 #
1177 def stylesheet_link_tag(*sources)
1177 def stylesheet_link_tag(*sources)
1178 options = sources.last.is_a?(Hash) ? sources.pop : {}
1178 options = sources.last.is_a?(Hash) ? sources.pop : {}
1179 plugin = options.delete(:plugin)
1179 plugin = options.delete(:plugin)
1180 sources = sources.map do |source|
1180 sources = sources.map do |source|
1181 if plugin
1181 if plugin
1182 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1182 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1183 elsif current_theme && current_theme.stylesheets.include?(source)
1183 elsif current_theme && current_theme.stylesheets.include?(source)
1184 current_theme.stylesheet_path(source)
1184 current_theme.stylesheet_path(source)
1185 else
1185 else
1186 source
1186 source
1187 end
1187 end
1188 end
1188 end
1189 super *sources, options
1189 super *sources, options
1190 end
1190 end
1191
1191
1192 # Overrides Rails' image_tag with themes and plugins support.
1192 # Overrides Rails' image_tag with themes and plugins support.
1193 # Examples:
1193 # Examples:
1194 # image_tag('image.png') # => picks image.png from the current theme or defaults
1194 # image_tag('image.png') # => picks image.png from the current theme or defaults
1195 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1195 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1196 #
1196 #
1197 def image_tag(source, options={})
1197 def image_tag(source, options={})
1198 if plugin = options.delete(:plugin)
1198 if plugin = options.delete(:plugin)
1199 source = "/plugin_assets/#{plugin}/images/#{source}"
1199 source = "/plugin_assets/#{plugin}/images/#{source}"
1200 elsif current_theme && current_theme.images.include?(source)
1200 elsif current_theme && current_theme.images.include?(source)
1201 source = current_theme.image_path(source)
1201 source = current_theme.image_path(source)
1202 end
1202 end
1203 super source, options
1203 super source, options
1204 end
1204 end
1205
1205
1206 # Overrides Rails' javascript_include_tag with plugins support
1206 # Overrides Rails' javascript_include_tag with plugins support
1207 # Examples:
1207 # Examples:
1208 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1208 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1209 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1209 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1210 #
1210 #
1211 def javascript_include_tag(*sources)
1211 def javascript_include_tag(*sources)
1212 options = sources.last.is_a?(Hash) ? sources.pop : {}
1212 options = sources.last.is_a?(Hash) ? sources.pop : {}
1213 if plugin = options.delete(:plugin)
1213 if plugin = options.delete(:plugin)
1214 sources = sources.map do |source|
1214 sources = sources.map do |source|
1215 if plugin
1215 if plugin
1216 "/plugin_assets/#{plugin}/javascripts/#{source}"
1216 "/plugin_assets/#{plugin}/javascripts/#{source}"
1217 else
1217 else
1218 source
1218 source
1219 end
1219 end
1220 end
1220 end
1221 end
1221 end
1222 super *sources, options
1222 super *sources, options
1223 end
1223 end
1224
1224
1225 def sidebar_content?
1225 def sidebar_content?
1226 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1226 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1227 end
1227 end
1228
1228
1229 def view_layouts_base_sidebar_hook_response
1229 def view_layouts_base_sidebar_hook_response
1230 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1230 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1231 end
1231 end
1232
1232
1233 def email_delivery_enabled?
1233 def email_delivery_enabled?
1234 !!ActionMailer::Base.perform_deliveries
1234 !!ActionMailer::Base.perform_deliveries
1235 end
1235 end
1236
1236
1237 # Returns the avatar image tag for the given +user+ if avatars are enabled
1237 # Returns the avatar image tag for the given +user+ if avatars are enabled
1238 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1238 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1239 def avatar(user, options = { })
1239 def avatar(user, options = { })
1240 if Setting.gravatar_enabled?
1240 if Setting.gravatar_enabled?
1241 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1241 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1242 email = nil
1242 email = nil
1243 if user.respond_to?(:mail)
1243 if user.respond_to?(:mail)
1244 email = user.mail
1244 email = user.mail
1245 elsif user.to_s =~ %r{<(.+?)>}
1245 elsif user.to_s =~ %r{<(.+?)>}
1246 email = $1
1246 email = $1
1247 end
1247 end
1248 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1248 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1249 else
1249 else
1250 ''
1250 ''
1251 end
1251 end
1252 end
1252 end
1253
1253
1254 # Returns a link to edit user's avatar if avatars are enabled
1254 # Returns a link to edit user's avatar if avatars are enabled
1255 def avatar_edit_link(user, options={})
1255 def avatar_edit_link(user, options={})
1256 if Setting.gravatar_enabled?
1256 if Setting.gravatar_enabled?
1257 url = "https://gravatar.com"
1257 url = "https://gravatar.com"
1258 link_to avatar(user, {:title => l(:button_edit)}.merge(options)), url, :target => '_blank'
1258 link_to avatar(user, {:title => l(:button_edit)}.merge(options)), url, :target => '_blank'
1259 end
1259 end
1260 end
1260 end
1261
1261
1262 def sanitize_anchor_name(anchor)
1262 def sanitize_anchor_name(anchor)
1263 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1263 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1264 end
1264 end
1265
1265
1266 # Returns the javascript tags that are included in the html layout head
1266 # Returns the javascript tags that are included in the html layout head
1267 def javascript_heads
1267 def javascript_heads
1268 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.3', 'application')
1268 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.3', 'application')
1269 unless User.current.pref.warn_on_leaving_unsaved == '0'
1269 unless User.current.pref.warn_on_leaving_unsaved == '0'
1270 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1270 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1271 end
1271 end
1272 tags
1272 tags
1273 end
1273 end
1274
1274
1275 def favicon
1275 def favicon
1276 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1276 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1277 end
1277 end
1278
1278
1279 # Returns the path to the favicon
1279 # Returns the path to the favicon
1280 def favicon_path
1280 def favicon_path
1281 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1281 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1282 image_path(icon)
1282 image_path(icon)
1283 end
1283 end
1284
1284
1285 # Returns the full URL to the favicon
1285 # Returns the full URL to the favicon
1286 def favicon_url
1286 def favicon_url
1287 # TODO: use #image_url introduced in Rails4
1287 # TODO: use #image_url introduced in Rails4
1288 path = favicon_path
1288 path = favicon_path
1289 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1289 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1290 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1290 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1291 end
1291 end
1292
1292
1293 def robot_exclusion_tag
1293 def robot_exclusion_tag
1294 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1294 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1295 end
1295 end
1296
1296
1297 # Returns true if arg is expected in the API response
1297 # Returns true if arg is expected in the API response
1298 def include_in_api_response?(arg)
1298 def include_in_api_response?(arg)
1299 unless @included_in_api_response
1299 unless @included_in_api_response
1300 param = params[:include]
1300 param = params[:include]
1301 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1301 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1302 @included_in_api_response.collect!(&:strip)
1302 @included_in_api_response.collect!(&:strip)
1303 end
1303 end
1304 @included_in_api_response.include?(arg.to_s)
1304 @included_in_api_response.include?(arg.to_s)
1305 end
1305 end
1306
1306
1307 # Returns options or nil if nometa param or X-Redmine-Nometa header
1307 # Returns options or nil if nometa param or X-Redmine-Nometa header
1308 # was set in the request
1308 # was set in the request
1309 def api_meta(options)
1309 def api_meta(options)
1310 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1310 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1311 # compatibility mode for activeresource clients that raise
1311 # compatibility mode for activeresource clients that raise
1312 # an error when deserializing an array with attributes
1312 # an error when deserializing an array with attributes
1313 nil
1313 nil
1314 else
1314 else
1315 options
1315 options
1316 end
1316 end
1317 end
1317 end
1318
1318
1319 def generate_csv(&block)
1319 def generate_csv(&block)
1320 decimal_separator = l(:general_csv_decimal_separator)
1320 decimal_separator = l(:general_csv_decimal_separator)
1321 encoding = l(:general_csv_encoding)
1321 encoding = l(:general_csv_encoding)
1322 end
1322 end
1323
1323
1324 private
1324 private
1325
1325
1326 def wiki_helper
1326 def wiki_helper
1327 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1327 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1328 extend helper
1328 extend helper
1329 return self
1329 return self
1330 end
1330 end
1331
1331
1332 def link_to_content_update(text, url_params = {}, html_options = {})
1332 def link_to_content_update(text, url_params = {}, html_options = {})
1333 link_to(text, url_params, html_options)
1333 link_to(text, url_params, html_options)
1334 end
1334 end
1335 end
1335 end
@@ -1,1528 +1,1528
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 require File.expand_path('../../../test_helper', __FILE__)
20 require File.expand_path('../../../test_helper', __FILE__)
21
21
22 class ApplicationHelperTest < ActionView::TestCase
22 class ApplicationHelperTest < ActionView::TestCase
23 include Redmine::I18n
23 include Redmine::I18n
24 include ERB::Util
24 include ERB::Util
25 include Rails.application.routes.url_helpers
25 include Rails.application.routes.url_helpers
26
26
27 fixtures :projects, :roles, :enabled_modules, :users,
27 fixtures :projects, :roles, :enabled_modules, :users,
28 :email_addresses,
28 :email_addresses,
29 :repositories, :changesets,
29 :repositories, :changesets,
30 :projects_trackers,
30 :projects_trackers,
31 :trackers, :issue_statuses, :issues, :versions, :documents,
31 :trackers, :issue_statuses, :issues, :versions, :documents,
32 :wikis, :wiki_pages, :wiki_contents,
32 :wikis, :wiki_pages, :wiki_contents,
33 :boards, :messages, :news,
33 :boards, :messages, :news,
34 :attachments, :enumerations
34 :attachments, :enumerations
35
35
36 def setup
36 def setup
37 super
37 super
38 set_tmp_attachments_directory
38 set_tmp_attachments_directory
39 @russian_test = "\xd1\x82\xd0\xb5\xd1\x81\xd1\x82".force_encoding('UTF-8')
39 @russian_test = "\xd1\x82\xd0\xb5\xd1\x81\xd1\x82".force_encoding('UTF-8')
40 end
40 end
41
41
42 test "#link_to_if_authorized for authorized user should allow using the :controller and :action for the target link" do
42 test "#link_to_if_authorized for authorized user should allow using the :controller and :action for the target link" do
43 User.current = User.find_by_login('admin')
43 User.current = User.find_by_login('admin')
44
44
45 @project = Issue.first.project # Used by helper
45 @project = Issue.first.project # Used by helper
46 response = link_to_if_authorized('By controller/actionr',
46 response = link_to_if_authorized('By controller/actionr',
47 {:controller => 'issues', :action => 'edit', :id => Issue.first.id})
47 {:controller => 'issues', :action => 'edit', :id => Issue.first.id})
48 assert_match /href/, response
48 assert_match /href/, response
49 end
49 end
50
50
51 test "#link_to_if_authorized for unauthorized user should display nothing if user isn't authorized" do
51 test "#link_to_if_authorized for unauthorized user should display nothing if user isn't authorized" do
52 User.current = User.find_by_login('dlopper')
52 User.current = User.find_by_login('dlopper')
53 @project = Project.find('private-child')
53 @project = Project.find('private-child')
54 issue = @project.issues.first
54 issue = @project.issues.first
55 assert !issue.visible?
55 assert !issue.visible?
56
56
57 response = link_to_if_authorized('Never displayed',
57 response = link_to_if_authorized('Never displayed',
58 {:controller => 'issues', :action => 'show', :id => issue})
58 {:controller => 'issues', :action => 'show', :id => issue})
59 assert_nil response
59 assert_nil response
60 end
60 end
61
61
62 def test_auto_links
62 def test_auto_links
63 to_test = {
63 to_test = {
64 'http://foo.bar' => '<a class="external" href="http://foo.bar">http://foo.bar</a>',
64 'http://foo.bar' => '<a class="external" href="http://foo.bar">http://foo.bar</a>',
65 'http://foo.bar/~user' => '<a class="external" href="http://foo.bar/~user">http://foo.bar/~user</a>',
65 'http://foo.bar/~user' => '<a class="external" href="http://foo.bar/~user">http://foo.bar/~user</a>',
66 'http://foo.bar.' => '<a class="external" href="http://foo.bar">http://foo.bar</a>.',
66 'http://foo.bar.' => '<a class="external" href="http://foo.bar">http://foo.bar</a>.',
67 'https://foo.bar.' => '<a class="external" href="https://foo.bar">https://foo.bar</a>.',
67 'https://foo.bar.' => '<a class="external" href="https://foo.bar">https://foo.bar</a>.',
68 'This is a link: http://foo.bar.' => 'This is a link: <a class="external" href="http://foo.bar">http://foo.bar</a>.',
68 'This is a link: http://foo.bar.' => 'This is a link: <a class="external" href="http://foo.bar">http://foo.bar</a>.',
69 'A link (eg. http://foo.bar).' => 'A link (eg. <a class="external" href="http://foo.bar">http://foo.bar</a>).',
69 'A link (eg. http://foo.bar).' => 'A link (eg. <a class="external" href="http://foo.bar">http://foo.bar</a>).',
70 'http://foo.bar/foo.bar#foo.bar.' => '<a class="external" href="http://foo.bar/foo.bar#foo.bar">http://foo.bar/foo.bar#foo.bar</a>.',
70 'http://foo.bar/foo.bar#foo.bar.' => '<a class="external" href="http://foo.bar/foo.bar#foo.bar">http://foo.bar/foo.bar#foo.bar</a>.',
71 'http://www.foo.bar/Test_(foobar)' => '<a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>',
71 'http://www.foo.bar/Test_(foobar)' => '<a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>',
72 '(see inline link : http://www.foo.bar/Test_(foobar))' => '(see inline link : <a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>)',
72 '(see inline link : http://www.foo.bar/Test_(foobar))' => '(see inline link : <a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>)',
73 '(see inline link : http://www.foo.bar/Test)' => '(see inline link : <a class="external" href="http://www.foo.bar/Test">http://www.foo.bar/Test</a>)',
73 '(see inline link : http://www.foo.bar/Test)' => '(see inline link : <a class="external" href="http://www.foo.bar/Test">http://www.foo.bar/Test</a>)',
74 '(see inline link : http://www.foo.bar/Test).' => '(see inline link : <a class="external" href="http://www.foo.bar/Test">http://www.foo.bar/Test</a>).',
74 '(see inline link : http://www.foo.bar/Test).' => '(see inline link : <a class="external" href="http://www.foo.bar/Test">http://www.foo.bar/Test</a>).',
75 '(see "inline link":http://www.foo.bar/Test_(foobar))' => '(see <a href="http://www.foo.bar/Test_(foobar)" class="external">inline link</a>)',
75 '(see "inline link":http://www.foo.bar/Test_(foobar))' => '(see <a href="http://www.foo.bar/Test_(foobar)" class="external">inline link</a>)',
76 '(see "inline link":http://www.foo.bar/Test)' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>)',
76 '(see "inline link":http://www.foo.bar/Test)' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>)',
77 '(see "inline link":http://www.foo.bar/Test).' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>).',
77 '(see "inline link":http://www.foo.bar/Test).' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>).',
78 'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>',
78 'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>',
79 'http://foo.bar/page?p=1&t=z&s=' => '<a class="external" href="http://foo.bar/page?p=1&#38;t=z&#38;s=">http://foo.bar/page?p=1&#38;t=z&#38;s=</a>',
79 'http://foo.bar/page?p=1&t=z&s=' => '<a class="external" href="http://foo.bar/page?p=1&#38;t=z&#38;s=">http://foo.bar/page?p=1&#38;t=z&#38;s=</a>',
80 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>',
80 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>',
81 'http://foo@www.bar.com' => '<a class="external" href="http://foo@www.bar.com">http://foo@www.bar.com</a>',
81 'http://foo@www.bar.com' => '<a class="external" href="http://foo@www.bar.com">http://foo@www.bar.com</a>',
82 'http://foo:bar@www.bar.com' => '<a class="external" href="http://foo:bar@www.bar.com">http://foo:bar@www.bar.com</a>',
82 'http://foo:bar@www.bar.com' => '<a class="external" href="http://foo:bar@www.bar.com">http://foo:bar@www.bar.com</a>',
83 'ftp://foo.bar' => '<a class="external" href="ftp://foo.bar">ftp://foo.bar</a>',
83 'ftp://foo.bar' => '<a class="external" href="ftp://foo.bar">ftp://foo.bar</a>',
84 'ftps://foo.bar' => '<a class="external" href="ftps://foo.bar">ftps://foo.bar</a>',
84 'ftps://foo.bar' => '<a class="external" href="ftps://foo.bar">ftps://foo.bar</a>',
85 'sftp://foo.bar' => '<a class="external" href="sftp://foo.bar">sftp://foo.bar</a>',
85 'sftp://foo.bar' => '<a class="external" href="sftp://foo.bar">sftp://foo.bar</a>',
86 # two exclamation marks
86 # two exclamation marks
87 'http://example.net/path!602815048C7B5C20!302.html' => '<a class="external" href="http://example.net/path!602815048C7B5C20!302.html">http://example.net/path!602815048C7B5C20!302.html</a>',
87 'http://example.net/path!602815048C7B5C20!302.html' => '<a class="external" href="http://example.net/path!602815048C7B5C20!302.html">http://example.net/path!602815048C7B5C20!302.html</a>',
88 # escaping
88 # escaping
89 'http://foo"bar' => '<a class="external" href="http://foo&quot;bar">http://foo&quot;bar</a>',
89 'http://foo"bar' => '<a class="external" href="http://foo&quot;bar">http://foo&quot;bar</a>',
90 # wrap in angle brackets
90 # wrap in angle brackets
91 '<http://foo.bar>' => '&lt;<a class="external" href="http://foo.bar">http://foo.bar</a>&gt;',
91 '<http://foo.bar>' => '&lt;<a class="external" href="http://foo.bar">http://foo.bar</a>&gt;',
92 # invalid urls
92 # invalid urls
93 'http://' => 'http://',
93 'http://' => 'http://',
94 'www.' => 'www.',
94 'www.' => 'www.',
95 'test-www.bar.com' => 'test-www.bar.com',
95 'test-www.bar.com' => 'test-www.bar.com',
96 }
96 }
97 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
97 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
98 end
98 end
99
99
100 def test_auto_links_with_non_ascii_characters
100 def test_auto_links_with_non_ascii_characters
101 to_test = {
101 to_test = {
102 "http://foo.bar/#{@russian_test}" =>
102 "http://foo.bar/#{@russian_test}" =>
103 %|<a class="external" href="http://foo.bar/#{@russian_test}">http://foo.bar/#{@russian_test}</a>|
103 %|<a class="external" href="http://foo.bar/#{@russian_test}">http://foo.bar/#{@russian_test}</a>|
104 }
104 }
105 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
105 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
106 end
106 end
107
107
108 def test_auto_mailto
108 def test_auto_mailto
109 to_test = {
109 to_test = {
110 'test@foo.bar' => '<a class="email" href="mailto:test@foo.bar">test@foo.bar</a>',
110 'test@foo.bar' => '<a class="email" href="mailto:test@foo.bar">test@foo.bar</a>',
111 'test@www.foo.bar' => '<a class="email" href="mailto:test@www.foo.bar">test@www.foo.bar</a>',
111 'test@www.foo.bar' => '<a class="email" href="mailto:test@www.foo.bar">test@www.foo.bar</a>',
112 }
112 }
113 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
113 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
114 end
114 end
115
115
116 def test_inline_images
116 def test_inline_images
117 to_test = {
117 to_test = {
118 '!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
118 '!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
119 'floating !>http://foo.bar/image.jpg!' => 'floating <div style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></div>',
119 'floating !>http://foo.bar/image.jpg!' => 'floating <div style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></div>',
120 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class <img src="http://foo.bar/image.jpg" class="some-class" alt="" />',
120 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class <img src="http://foo.bar/image.jpg" class="some-class" alt="" />',
121 'with style !{width:100px;height:100px}http://foo.bar/image.jpg!' => 'with style <img src="http://foo.bar/image.jpg" style="width:100px;height:100px;" alt="" />',
121 'with style !{width:100px;height:100px}http://foo.bar/image.jpg!' => 'with style <img src="http://foo.bar/image.jpg" style="width:100px;height:100px;" alt="" />',
122 'with title !http://foo.bar/image.jpg(This is a title)!' => 'with title <img src="http://foo.bar/image.jpg" title="This is a title" alt="This is a title" />',
122 'with title !http://foo.bar/image.jpg(This is a title)!' => 'with title <img src="http://foo.bar/image.jpg" title="This is a title" alt="This is a title" />',
123 'with title !http://foo.bar/image.jpg(This is a double-quoted "title")!' => 'with title <img src="http://foo.bar/image.jpg" title="This is a double-quoted &quot;title&quot;" alt="This is a double-quoted &quot;title&quot;" />',
123 'with title !http://foo.bar/image.jpg(This is a double-quoted "title")!' => 'with title <img src="http://foo.bar/image.jpg" title="This is a double-quoted &quot;title&quot;" alt="This is a double-quoted &quot;title&quot;" />',
124 }
124 }
125 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
125 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
126 end
126 end
127
127
128 def test_inline_images_inside_tags
128 def test_inline_images_inside_tags
129 raw = <<-RAW
129 raw = <<-RAW
130 h1. !foo.png! Heading
130 h1. !foo.png! Heading
131
131
132 Centered image:
132 Centered image:
133
133
134 p=. !bar.gif!
134 p=. !bar.gif!
135 RAW
135 RAW
136
136
137 assert textilizable(raw).include?('<img src="foo.png" alt="" />')
137 assert textilizable(raw).include?('<img src="foo.png" alt="" />')
138 assert textilizable(raw).include?('<img src="bar.gif" alt="" />')
138 assert textilizable(raw).include?('<img src="bar.gif" alt="" />')
139 end
139 end
140
140
141 def test_attached_images
141 def test_attached_images
142 to_test = {
142 to_test = {
143 'Inline image: !logo.gif!' => 'Inline image: <img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" />',
143 'Inline image: !logo.gif!' => 'Inline image: <img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" />',
144 'Inline image: !logo.GIF!' => 'Inline image: <img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" />',
144 'Inline image: !logo.GIF!' => 'Inline image: <img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" />',
145 'No match: !ogo.gif!' => 'No match: <img src="ogo.gif" alt="" />',
145 'No match: !ogo.gif!' => 'No match: <img src="ogo.gif" alt="" />',
146 'No match: !ogo.GIF!' => 'No match: <img src="ogo.GIF" alt="" />',
146 'No match: !ogo.GIF!' => 'No match: <img src="ogo.GIF" alt="" />',
147 # link image
147 # link image
148 '!logo.gif!:http://foo.bar/' => '<a href="http://foo.bar/"><img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" /></a>',
148 '!logo.gif!:http://foo.bar/' => '<a href="http://foo.bar/"><img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" /></a>',
149 }
149 }
150 attachments = Attachment.all
150 attachments = Attachment.all
151 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
151 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
152 end
152 end
153
153
154 def test_attached_images_with_textile_and_non_ascii_filename
154 def test_attached_images_with_textile_and_non_ascii_filename
155 attachment = Attachment.generate!(:filename => 'cafΓ©.jpg')
155 attachment = Attachment.generate!(:filename => 'cafΓ©.jpg')
156 with_settings :text_formatting => 'textile' do
156 with_settings :text_formatting => 'textile' do
157 assert_include %(<img src="/attachments/download/#{attachment.id}/caf%C3%A9.jpg" alt="" />),
157 assert_include %(<img src="/attachments/download/#{attachment.id}/caf%C3%A9.jpg" alt="" />),
158 textilizable("!cafΓ©.jpg!)", :attachments => [attachment])
158 textilizable("!cafΓ©.jpg!)", :attachments => [attachment])
159 end
159 end
160 end
160 end
161
161
162 def test_attached_images_with_markdown_and_non_ascii_filename
162 def test_attached_images_with_markdown_and_non_ascii_filename
163 skip unless Object.const_defined?(:Redcarpet)
163 skip unless Object.const_defined?(:Redcarpet)
164
164
165 attachment = Attachment.generate!(:filename => 'cafΓ©.jpg')
165 attachment = Attachment.generate!(:filename => 'cafΓ©.jpg')
166 with_settings :text_formatting => 'markdown' do
166 with_settings :text_formatting => 'markdown' do
167 assert_include %(<img src="/attachments/download/#{attachment.id}/caf%C3%A9.jpg" alt="">),
167 assert_include %(<img src="/attachments/download/#{attachment.id}/caf%C3%A9.jpg" alt="">),
168 textilizable("![](cafΓ©.jpg)", :attachments => [attachment])
168 textilizable("![](cafΓ©.jpg)", :attachments => [attachment])
169 end
169 end
170 end
170 end
171
171
172 def test_attached_images_filename_extension
172 def test_attached_images_filename_extension
173 set_tmp_attachments_directory
173 set_tmp_attachments_directory
174 a1 = Attachment.new(
174 a1 = Attachment.new(
175 :container => Issue.find(1),
175 :container => Issue.find(1),
176 :file => mock_file_with_options({:original_filename => "testtest.JPG"}),
176 :file => mock_file_with_options({:original_filename => "testtest.JPG"}),
177 :author => User.find(1))
177 :author => User.find(1))
178 assert a1.save
178 assert a1.save
179 assert_equal "testtest.JPG", a1.filename
179 assert_equal "testtest.JPG", a1.filename
180 assert_equal "image/jpeg", a1.content_type
180 assert_equal "image/jpeg", a1.content_type
181 assert a1.image?
181 assert a1.image?
182
182
183 a2 = Attachment.new(
183 a2 = Attachment.new(
184 :container => Issue.find(1),
184 :container => Issue.find(1),
185 :file => mock_file_with_options({:original_filename => "testtest.jpeg"}),
185 :file => mock_file_with_options({:original_filename => "testtest.jpeg"}),
186 :author => User.find(1))
186 :author => User.find(1))
187 assert a2.save
187 assert a2.save
188 assert_equal "testtest.jpeg", a2.filename
188 assert_equal "testtest.jpeg", a2.filename
189 assert_equal "image/jpeg", a2.content_type
189 assert_equal "image/jpeg", a2.content_type
190 assert a2.image?
190 assert a2.image?
191
191
192 a3 = Attachment.new(
192 a3 = Attachment.new(
193 :container => Issue.find(1),
193 :container => Issue.find(1),
194 :file => mock_file_with_options({:original_filename => "testtest.JPE"}),
194 :file => mock_file_with_options({:original_filename => "testtest.JPE"}),
195 :author => User.find(1))
195 :author => User.find(1))
196 assert a3.save
196 assert a3.save
197 assert_equal "testtest.JPE", a3.filename
197 assert_equal "testtest.JPE", a3.filename
198 assert_equal "image/jpeg", a3.content_type
198 assert_equal "image/jpeg", a3.content_type
199 assert a3.image?
199 assert a3.image?
200
200
201 a4 = Attachment.new(
201 a4 = Attachment.new(
202 :container => Issue.find(1),
202 :container => Issue.find(1),
203 :file => mock_file_with_options({:original_filename => "Testtest.BMP"}),
203 :file => mock_file_with_options({:original_filename => "Testtest.BMP"}),
204 :author => User.find(1))
204 :author => User.find(1))
205 assert a4.save
205 assert a4.save
206 assert_equal "Testtest.BMP", a4.filename
206 assert_equal "Testtest.BMP", a4.filename
207 assert_equal "image/x-ms-bmp", a4.content_type
207 assert_equal "image/x-ms-bmp", a4.content_type
208 assert a4.image?
208 assert a4.image?
209
209
210 to_test = {
210 to_test = {
211 'Inline image: !testtest.jpg!' =>
211 'Inline image: !testtest.jpg!' =>
212 'Inline image: <img src="/attachments/download/' + a1.id.to_s + '/testtest.JPG" alt="" />',
212 'Inline image: <img src="/attachments/download/' + a1.id.to_s + '/testtest.JPG" alt="" />',
213 'Inline image: !testtest.jpeg!' =>
213 'Inline image: !testtest.jpeg!' =>
214 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testtest.jpeg" alt="" />',
214 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testtest.jpeg" alt="" />',
215 'Inline image: !testtest.jpe!' =>
215 'Inline image: !testtest.jpe!' =>
216 'Inline image: <img src="/attachments/download/' + a3.id.to_s + '/testtest.JPE" alt="" />',
216 'Inline image: <img src="/attachments/download/' + a3.id.to_s + '/testtest.JPE" alt="" />',
217 'Inline image: !testtest.bmp!' =>
217 'Inline image: !testtest.bmp!' =>
218 'Inline image: <img src="/attachments/download/' + a4.id.to_s + '/Testtest.BMP" alt="" />',
218 'Inline image: <img src="/attachments/download/' + a4.id.to_s + '/Testtest.BMP" alt="" />',
219 }
219 }
220
220
221 attachments = [a1, a2, a3, a4]
221 attachments = [a1, a2, a3, a4]
222 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
222 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
223 end
223 end
224
224
225 def test_attached_images_should_read_later
225 def test_attached_images_should_read_later
226 set_fixtures_attachments_directory
226 set_fixtures_attachments_directory
227 a1 = Attachment.find(16)
227 a1 = Attachment.find(16)
228 assert_equal "testfile.png", a1.filename
228 assert_equal "testfile.png", a1.filename
229 assert a1.readable?
229 assert a1.readable?
230 assert (! a1.visible?(User.anonymous))
230 assert (! a1.visible?(User.anonymous))
231 assert a1.visible?(User.find(2))
231 assert a1.visible?(User.find(2))
232 a2 = Attachment.find(17)
232 a2 = Attachment.find(17)
233 assert_equal "testfile.PNG", a2.filename
233 assert_equal "testfile.PNG", a2.filename
234 assert a2.readable?
234 assert a2.readable?
235 assert (! a2.visible?(User.anonymous))
235 assert (! a2.visible?(User.anonymous))
236 assert a2.visible?(User.find(2))
236 assert a2.visible?(User.find(2))
237 assert a1.created_on < a2.created_on
237 assert a1.created_on < a2.created_on
238
238
239 to_test = {
239 to_test = {
240 'Inline image: !testfile.png!' =>
240 'Inline image: !testfile.png!' =>
241 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testfile.PNG" alt="" />',
241 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testfile.PNG" alt="" />',
242 'Inline image: !Testfile.PNG!' =>
242 'Inline image: !Testfile.PNG!' =>
243 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testfile.PNG" alt="" />',
243 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testfile.PNG" alt="" />',
244 }
244 }
245 attachments = [a1, a2]
245 attachments = [a1, a2]
246 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
246 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
247 set_tmp_attachments_directory
247 set_tmp_attachments_directory
248 end
248 end
249
249
250 def test_textile_external_links
250 def test_textile_external_links
251 to_test = {
251 to_test = {
252 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>',
252 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>',
253 'This is an intern "link":/foo/bar' => 'This is an intern <a href="/foo/bar">link</a>',
253 'This is an intern "link":/foo/bar' => 'This is an intern <a href="/foo/bar">link</a>',
254 '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>',
254 '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>',
255 '"link (Link title with "double-quotes")":http://foo.bar' => '<a href="http://foo.bar" title="Link title with &quot;double-quotes&quot;" class="external">link</a>',
255 '"link (Link title with "double-quotes")":http://foo.bar' => '<a href="http://foo.bar" title="Link title with &quot;double-quotes&quot;" class="external">link</a>',
256 "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":</p>\n\n\n\t<p>Another paragraph",
256 "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":</p>\n\n\n\t<p>Another paragraph",
257 # no multiline link text
257 # no multiline link text
258 "This is a double quote \"on the first line\nand another on a second line\":test" => "This is a double quote \"on the first line<br />and another on a second line\":test",
258 "This is a double quote \"on the first line\nand another on a second line\":test" => "This is a double quote \"on the first line<br />and another on a second line\":test",
259 # mailto link
259 # mailto link
260 "\"system administrator\":mailto:sysadmin@example.com?subject=redmine%20permissions" => "<a href=\"mailto:sysadmin@example.com?subject=redmine%20permissions\">system administrator</a>",
260 "\"system administrator\":mailto:sysadmin@example.com?subject=redmine%20permissions" => "<a href=\"mailto:sysadmin@example.com?subject=redmine%20permissions\">system administrator</a>",
261 # two exclamation marks
261 # two exclamation marks
262 '"a link":http://example.net/path!602815048C7B5C20!302.html' => '<a href="http://example.net/path!602815048C7B5C20!302.html" class="external">a link</a>',
262 '"a link":http://example.net/path!602815048C7B5C20!302.html' => '<a href="http://example.net/path!602815048C7B5C20!302.html" class="external">a link</a>',
263 # escaping
263 # escaping
264 '"test":http://foo"bar' => '<a href="http://foo&quot;bar" class="external">test</a>',
264 '"test":http://foo"bar' => '<a href="http://foo&quot;bar" class="external">test</a>',
265 }
265 }
266 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
266 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
267 end
267 end
268
268
269 def test_textile_external_links_with_non_ascii_characters
269 def test_textile_external_links_with_non_ascii_characters
270 to_test = {
270 to_test = {
271 %|This is a "link":http://foo.bar/#{@russian_test}| =>
271 %|This is a "link":http://foo.bar/#{@russian_test}| =>
272 %|This is a <a href="http://foo.bar/#{@russian_test}" class="external">link</a>|
272 %|This is a <a href="http://foo.bar/#{@russian_test}" class="external">link</a>|
273 }
273 }
274 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
274 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
275 end
275 end
276
276
277 def test_redmine_links
277 def test_redmine_links
278 issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3},
278 issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3},
279 :class => Issue.find(3).css_classes, :title => 'Error 281 when updating a recipe (New)')
279 :class => Issue.find(3).css_classes, :title => 'Bug: Error 281 when updating a recipe (New)')
280 note_link = link_to('#3-14', {:controller => 'issues', :action => 'show', :id => 3, :anchor => 'note-14'},
280 note_link = link_to('#3-14', {:controller => 'issues', :action => 'show', :id => 3, :anchor => 'note-14'},
281 :class => Issue.find(3).css_classes, :title => 'Error 281 when updating a recipe (New)')
281 :class => Issue.find(3).css_classes, :title => 'Bug: Error 281 when updating a recipe (New)')
282 note_link2 = link_to('#3#note-14', {:controller => 'issues', :action => 'show', :id => 3, :anchor => 'note-14'},
282 note_link2 = link_to('#3#note-14', {:controller => 'issues', :action => 'show', :id => 3, :anchor => 'note-14'},
283 :class => Issue.find(3).css_classes, :title => 'Error 281 when updating a recipe (New)')
283 :class => Issue.find(3).css_classes, :title => 'Bug: Error 281 when updating a recipe (New)')
284
284
285 revision_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
285 revision_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
286 :class => 'changeset', :title => 'My very first commit do not escaping #<>&')
286 :class => 'changeset', :title => 'My very first commit do not escaping #<>&')
287 revision_link2 = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
287 revision_link2 = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
288 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
288 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
289
289
290 changeset_link2 = link_to('691322a8eb01e11fd7',
290 changeset_link2 = link_to('691322a8eb01e11fd7',
291 {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
291 {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
292 :class => 'changeset', :title => 'My very first commit do not escaping #<>&')
292 :class => 'changeset', :title => 'My very first commit do not escaping #<>&')
293
293
294 document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
294 document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
295 :class => 'document')
295 :class => 'document')
296
296
297 version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
297 version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
298 :class => 'version')
298 :class => 'version')
299
299
300 board_url = {:controller => 'boards', :action => 'show', :id => 2, :project_id => 'ecookbook'}
300 board_url = {:controller => 'boards', :action => 'show', :id => 2, :project_id => 'ecookbook'}
301
301
302 message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4}
302 message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4}
303
303
304 news_url = {:controller => 'news', :action => 'show', :id => 1}
304 news_url = {:controller => 'news', :action => 'show', :id => 1}
305
305
306 project_url = {:controller => 'projects', :action => 'show', :id => 'subproject1'}
306 project_url = {:controller => 'projects', :action => 'show', :id => 'subproject1'}
307
307
308 source_url = '/projects/ecookbook/repository/entry/some/file'
308 source_url = '/projects/ecookbook/repository/entry/some/file'
309 source_url_with_rev = '/projects/ecookbook/repository/revisions/52/entry/some/file'
309 source_url_with_rev = '/projects/ecookbook/repository/revisions/52/entry/some/file'
310 source_url_with_ext = '/projects/ecookbook/repository/entry/some/file.ext'
310 source_url_with_ext = '/projects/ecookbook/repository/entry/some/file.ext'
311 source_url_with_rev_and_ext = '/projects/ecookbook/repository/revisions/52/entry/some/file.ext'
311 source_url_with_rev_and_ext = '/projects/ecookbook/repository/revisions/52/entry/some/file.ext'
312 source_url_with_branch = '/projects/ecookbook/repository/revisions/branch/entry/some/file'
312 source_url_with_branch = '/projects/ecookbook/repository/revisions/branch/entry/some/file'
313
313
314 export_url = '/projects/ecookbook/repository/raw/some/file'
314 export_url = '/projects/ecookbook/repository/raw/some/file'
315 export_url_with_rev = '/projects/ecookbook/repository/revisions/52/raw/some/file'
315 export_url_with_rev = '/projects/ecookbook/repository/revisions/52/raw/some/file'
316 export_url_with_ext = '/projects/ecookbook/repository/raw/some/file.ext'
316 export_url_with_ext = '/projects/ecookbook/repository/raw/some/file.ext'
317 export_url_with_rev_and_ext = '/projects/ecookbook/repository/revisions/52/raw/some/file.ext'
317 export_url_with_rev_and_ext = '/projects/ecookbook/repository/revisions/52/raw/some/file.ext'
318 export_url_with_branch = '/projects/ecookbook/repository/revisions/branch/raw/some/file'
318 export_url_with_branch = '/projects/ecookbook/repository/revisions/branch/raw/some/file'
319
319
320 to_test = {
320 to_test = {
321 # tickets
321 # tickets
322 '#3, [#3], (#3) and #3.' => "#{issue_link}, [#{issue_link}], (#{issue_link}) and #{issue_link}.",
322 '#3, [#3], (#3) and #3.' => "#{issue_link}, [#{issue_link}], (#{issue_link}) and #{issue_link}.",
323 # ticket notes
323 # ticket notes
324 '#3-14' => note_link,
324 '#3-14' => note_link,
325 '#3#note-14' => note_link2,
325 '#3#note-14' => note_link2,
326 # should not ignore leading zero
326 # should not ignore leading zero
327 '#03' => '#03',
327 '#03' => '#03',
328 # changesets
328 # changesets
329 'r1' => revision_link,
329 'r1' => revision_link,
330 'r1.' => "#{revision_link}.",
330 'r1.' => "#{revision_link}.",
331 'r1, r2' => "#{revision_link}, #{revision_link2}",
331 'r1, r2' => "#{revision_link}, #{revision_link2}",
332 'r1,r2' => "#{revision_link},#{revision_link2}",
332 'r1,r2' => "#{revision_link},#{revision_link2}",
333 'commit:691322a8eb01e11fd7' => changeset_link2,
333 'commit:691322a8eb01e11fd7' => changeset_link2,
334 # documents
334 # documents
335 'document#1' => document_link,
335 'document#1' => document_link,
336 'document:"Test document"' => document_link,
336 'document:"Test document"' => document_link,
337 # versions
337 # versions
338 'version#2' => version_link,
338 'version#2' => version_link,
339 'version:1.0' => version_link,
339 'version:1.0' => version_link,
340 'version:"1.0"' => version_link,
340 'version:"1.0"' => version_link,
341 # source
341 # source
342 'source:some/file' => link_to('source:some/file', source_url, :class => 'source'),
342 'source:some/file' => link_to('source:some/file', source_url, :class => 'source'),
343 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
343 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
344 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".",
344 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".",
345 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
345 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
346 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".",
346 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".",
347 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
347 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
348 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",",
348 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",",
349 'source:/some/file@52' => link_to('source:/some/file@52', source_url_with_rev, :class => 'source'),
349 'source:/some/file@52' => link_to('source:/some/file@52', source_url_with_rev, :class => 'source'),
350 'source:/some/file@branch' => link_to('source:/some/file@branch', source_url_with_branch, :class => 'source'),
350 'source:/some/file@branch' => link_to('source:/some/file@branch', source_url_with_branch, :class => 'source'),
351 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_rev_and_ext, :class => 'source'),
351 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_rev_and_ext, :class => 'source'),
352 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url + "#L110", :class => 'source'),
352 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url + "#L110", :class => 'source'),
353 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext + "#L110", :class => 'source'),
353 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext + "#L110", :class => 'source'),
354 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url_with_rev + "#L110", :class => 'source'),
354 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url_with_rev + "#L110", :class => 'source'),
355 # export
355 # export
356 'export:/some/file' => link_to('export:/some/file', export_url, :class => 'source download'),
356 'export:/some/file' => link_to('export:/some/file', export_url, :class => 'source download'),
357 'export:/some/file.ext' => link_to('export:/some/file.ext', export_url_with_ext, :class => 'source download'),
357 'export:/some/file.ext' => link_to('export:/some/file.ext', export_url_with_ext, :class => 'source download'),
358 'export:/some/file@52' => link_to('export:/some/file@52', export_url_with_rev, :class => 'source download'),
358 'export:/some/file@52' => link_to('export:/some/file@52', export_url_with_rev, :class => 'source download'),
359 'export:/some/file.ext@52' => link_to('export:/some/file.ext@52', export_url_with_rev_and_ext, :class => 'source download'),
359 'export:/some/file.ext@52' => link_to('export:/some/file.ext@52', export_url_with_rev_and_ext, :class => 'source download'),
360 'export:/some/file@branch' => link_to('export:/some/file@branch', export_url_with_branch, :class => 'source download'),
360 'export:/some/file@branch' => link_to('export:/some/file@branch', export_url_with_branch, :class => 'source download'),
361 # forum
361 # forum
362 'forum#2' => link_to('Discussion', board_url, :class => 'board'),
362 'forum#2' => link_to('Discussion', board_url, :class => 'board'),
363 'forum:Discussion' => link_to('Discussion', board_url, :class => 'board'),
363 'forum:Discussion' => link_to('Discussion', board_url, :class => 'board'),
364 # message
364 # message
365 'message#4' => link_to('Post 2', message_url, :class => 'message'),
365 'message#4' => link_to('Post 2', message_url, :class => 'message'),
366 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5', :r => 5), :class => 'message'),
366 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5', :r => 5), :class => 'message'),
367 # news
367 # news
368 'news#1' => link_to('eCookbook first release !', news_url, :class => 'news'),
368 'news#1' => link_to('eCookbook first release !', news_url, :class => 'news'),
369 'news:"eCookbook first release !"' => link_to('eCookbook first release !', news_url, :class => 'news'),
369 'news:"eCookbook first release !"' => link_to('eCookbook first release !', news_url, :class => 'news'),
370 # project
370 # project
371 'project#3' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
371 'project#3' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
372 'project:subproject1' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
372 'project:subproject1' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
373 'project:"eCookbook subProject 1"' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
373 'project:"eCookbook subProject 1"' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
374 # not found
374 # not found
375 '#0123456789' => '#0123456789',
375 '#0123456789' => '#0123456789',
376 # invalid expressions
376 # invalid expressions
377 'source:' => 'source:',
377 'source:' => 'source:',
378 # url hash
378 # url hash
379 "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
379 "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
380 }
380 }
381 @project = Project.find(1)
381 @project = Project.find(1)
382 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
382 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
383 end
383 end
384
384
385 def test_should_not_parse_redmine_links_inside_link
385 def test_should_not_parse_redmine_links_inside_link
386 raw = "r1 should not be parsed in http://example.com/url-r1/"
386 raw = "r1 should not be parsed in http://example.com/url-r1/"
387 assert_match %r{<p><a class="changeset".*>r1</a> should not be parsed in <a class="external" href="http://example.com/url-r1/">http://example.com/url-r1/</a></p>},
387 assert_match %r{<p><a class="changeset".*>r1</a> should not be parsed in <a class="external" href="http://example.com/url-r1/">http://example.com/url-r1/</a></p>},
388 textilizable(raw, :project => Project.find(1))
388 textilizable(raw, :project => Project.find(1))
389 end
389 end
390
390
391 def test_redmine_links_with_a_different_project_before_current_project
391 def test_redmine_links_with_a_different_project_before_current_project
392 vp1 = Version.generate!(:project_id => 1, :name => '1.4.4')
392 vp1 = Version.generate!(:project_id => 1, :name => '1.4.4')
393 vp3 = Version.generate!(:project_id => 3, :name => '1.4.4')
393 vp3 = Version.generate!(:project_id => 3, :name => '1.4.4')
394 @project = Project.find(3)
394 @project = Project.find(3)
395 result1 = link_to("1.4.4", "/versions/#{vp1.id}", :class => "version")
395 result1 = link_to("1.4.4", "/versions/#{vp1.id}", :class => "version")
396 result2 = link_to("1.4.4", "/versions/#{vp3.id}", :class => "version")
396 result2 = link_to("1.4.4", "/versions/#{vp3.id}", :class => "version")
397 assert_equal "<p>#{result1} #{result2}</p>",
397 assert_equal "<p>#{result1} #{result2}</p>",
398 textilizable("ecookbook:version:1.4.4 version:1.4.4")
398 textilizable("ecookbook:version:1.4.4 version:1.4.4")
399 end
399 end
400
400
401 def test_escaped_redmine_links_should_not_be_parsed
401 def test_escaped_redmine_links_should_not_be_parsed
402 to_test = [
402 to_test = [
403 '#3.',
403 '#3.',
404 '#3-14.',
404 '#3-14.',
405 '#3#-note14.',
405 '#3#-note14.',
406 'r1',
406 'r1',
407 'document#1',
407 'document#1',
408 'document:"Test document"',
408 'document:"Test document"',
409 'version#2',
409 'version#2',
410 'version:1.0',
410 'version:1.0',
411 'version:"1.0"',
411 'version:"1.0"',
412 'source:/some/file'
412 'source:/some/file'
413 ]
413 ]
414 @project = Project.find(1)
414 @project = Project.find(1)
415 to_test.each { |text| assert_equal "<p>#{text}</p>", textilizable("!" + text), "#{text} failed" }
415 to_test.each { |text| assert_equal "<p>#{text}</p>", textilizable("!" + text), "#{text} failed" }
416 end
416 end
417
417
418 def test_cross_project_redmine_links
418 def test_cross_project_redmine_links
419 source_link = link_to('ecookbook:source:/some/file',
419 source_link = link_to('ecookbook:source:/some/file',
420 {:controller => 'repositories', :action => 'entry',
420 {:controller => 'repositories', :action => 'entry',
421 :id => 'ecookbook', :path => ['some', 'file']},
421 :id => 'ecookbook', :path => ['some', 'file']},
422 :class => 'source')
422 :class => 'source')
423 changeset_link = link_to('ecookbook:r2',
423 changeset_link = link_to('ecookbook:r2',
424 {:controller => 'repositories', :action => 'revision',
424 {:controller => 'repositories', :action => 'revision',
425 :id => 'ecookbook', :rev => 2},
425 :id => 'ecookbook', :rev => 2},
426 :class => 'changeset',
426 :class => 'changeset',
427 :title => 'This commit fixes #1, #2 and references #1 & #3')
427 :title => 'This commit fixes #1, #2 and references #1 & #3')
428 to_test = {
428 to_test = {
429 # documents
429 # documents
430 'document:"Test document"' => 'document:"Test document"',
430 'document:"Test document"' => 'document:"Test document"',
431 'ecookbook:document:"Test document"' =>
431 'ecookbook:document:"Test document"' =>
432 link_to("Test document", "/documents/1", :class => "document"),
432 link_to("Test document", "/documents/1", :class => "document"),
433 'invalid:document:"Test document"' => 'invalid:document:"Test document"',
433 'invalid:document:"Test document"' => 'invalid:document:"Test document"',
434 # versions
434 # versions
435 'version:"1.0"' => 'version:"1.0"',
435 'version:"1.0"' => 'version:"1.0"',
436 'ecookbook:version:"1.0"' =>
436 'ecookbook:version:"1.0"' =>
437 link_to("1.0", "/versions/2", :class => "version"),
437 link_to("1.0", "/versions/2", :class => "version"),
438 'invalid:version:"1.0"' => 'invalid:version:"1.0"',
438 'invalid:version:"1.0"' => 'invalid:version:"1.0"',
439 # changeset
439 # changeset
440 'r2' => 'r2',
440 'r2' => 'r2',
441 'ecookbook:r2' => changeset_link,
441 'ecookbook:r2' => changeset_link,
442 'invalid:r2' => 'invalid:r2',
442 'invalid:r2' => 'invalid:r2',
443 # source
443 # source
444 'source:/some/file' => 'source:/some/file',
444 'source:/some/file' => 'source:/some/file',
445 'ecookbook:source:/some/file' => source_link,
445 'ecookbook:source:/some/file' => source_link,
446 'invalid:source:/some/file' => 'invalid:source:/some/file',
446 'invalid:source:/some/file' => 'invalid:source:/some/file',
447 }
447 }
448 @project = Project.find(3)
448 @project = Project.find(3)
449 to_test.each do |text, result|
449 to_test.each do |text, result|
450 assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed"
450 assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed"
451 end
451 end
452 end
452 end
453
453
454 def test_redmine_links_by_name_should_work_with_html_escaped_characters
454 def test_redmine_links_by_name_should_work_with_html_escaped_characters
455 v = Version.generate!(:name => "Test & Show.txt", :project_id => 1)
455 v = Version.generate!(:name => "Test & Show.txt", :project_id => 1)
456 link = link_to("Test & Show.txt", "/versions/#{v.id}", :class => "version")
456 link = link_to("Test & Show.txt", "/versions/#{v.id}", :class => "version")
457
457
458 @project = v.project
458 @project = v.project
459 assert_equal "<p>#{link}</p>", textilizable('version:"Test & Show.txt"')
459 assert_equal "<p>#{link}</p>", textilizable('version:"Test & Show.txt"')
460 end
460 end
461
461
462 def test_link_to_issue_subject
462 def test_link_to_issue_subject
463 issue = Issue.generate!(:subject => "01234567890123456789")
463 issue = Issue.generate!(:subject => "01234567890123456789")
464 str = link_to_issue(issue, :truncate => 10)
464 str = link_to_issue(issue, :truncate => 10)
465 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}", :class => issue.css_classes)
465 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}", :class => issue.css_classes)
466 assert_equal "#{result}: 0123456...", str
466 assert_equal "#{result}: 0123456...", str
467
467
468 issue = Issue.generate!(:subject => "<&>")
468 issue = Issue.generate!(:subject => "<&>")
469 str = link_to_issue(issue)
469 str = link_to_issue(issue)
470 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}", :class => issue.css_classes)
470 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}", :class => issue.css_classes)
471 assert_equal "#{result}: &lt;&amp;&gt;", str
471 assert_equal "#{result}: &lt;&amp;&gt;", str
472
472
473 issue = Issue.generate!(:subject => "<&>0123456789012345")
473 issue = Issue.generate!(:subject => "<&>0123456789012345")
474 str = link_to_issue(issue, :truncate => 10)
474 str = link_to_issue(issue, :truncate => 10)
475 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}", :class => issue.css_classes)
475 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}", :class => issue.css_classes)
476 assert_equal "#{result}: &lt;&amp;&gt;0123...", str
476 assert_equal "#{result}: &lt;&amp;&gt;0123...", str
477 end
477 end
478
478
479 def test_link_to_issue_title
479 def test_link_to_issue_title
480 long_str = "0123456789" * 5
480 long_str = "0123456789" * 5
481
481
482 issue = Issue.generate!(:subject => "#{long_str}01234567890123456789")
482 issue = Issue.generate!(:subject => "#{long_str}01234567890123456789")
483 str = link_to_issue(issue, :subject => false)
483 str = link_to_issue(issue, :subject => false)
484 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}",
484 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}",
485 :class => issue.css_classes,
485 :class => issue.css_classes,
486 :title => "#{long_str}0123456...")
486 :title => "#{long_str}0123456...")
487 assert_equal result, str
487 assert_equal result, str
488
488
489 issue = Issue.generate!(:subject => "<&>#{long_str}01234567890123456789")
489 issue = Issue.generate!(:subject => "<&>#{long_str}01234567890123456789")
490 str = link_to_issue(issue, :subject => false)
490 str = link_to_issue(issue, :subject => false)
491 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}",
491 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}",
492 :class => issue.css_classes,
492 :class => issue.css_classes,
493 :title => "<&>#{long_str}0123...")
493 :title => "<&>#{long_str}0123...")
494 assert_equal result, str
494 assert_equal result, str
495 end
495 end
496
496
497 def test_multiple_repositories_redmine_links
497 def test_multiple_repositories_redmine_links
498 svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn_repo-1', :url => 'file:///foo/hg')
498 svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn_repo-1', :url => 'file:///foo/hg')
499 Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
499 Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
500 hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
500 hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
501 Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
501 Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
502
502
503 changeset_link = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
503 changeset_link = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
504 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
504 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
505 svn_changeset_link = link_to('svn_repo-1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn_repo-1', :rev => 123},
505 svn_changeset_link = link_to('svn_repo-1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn_repo-1', :rev => 123},
506 :class => 'changeset', :title => '')
506 :class => 'changeset', :title => '')
507 hg_changeset_link = link_to('hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
507 hg_changeset_link = link_to('hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
508 :class => 'changeset', :title => '')
508 :class => 'changeset', :title => '')
509
509
510 source_link = link_to('source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
510 source_link = link_to('source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
511 hg_source_link = link_to('source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
511 hg_source_link = link_to('source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
512
512
513 to_test = {
513 to_test = {
514 'r2' => changeset_link,
514 'r2' => changeset_link,
515 'svn_repo-1|r123' => svn_changeset_link,
515 'svn_repo-1|r123' => svn_changeset_link,
516 'invalid|r123' => 'invalid|r123',
516 'invalid|r123' => 'invalid|r123',
517 'commit:hg1|abcd' => hg_changeset_link,
517 'commit:hg1|abcd' => hg_changeset_link,
518 'commit:invalid|abcd' => 'commit:invalid|abcd',
518 'commit:invalid|abcd' => 'commit:invalid|abcd',
519 # source
519 # source
520 'source:some/file' => source_link,
520 'source:some/file' => source_link,
521 'source:hg1|some/file' => hg_source_link,
521 'source:hg1|some/file' => hg_source_link,
522 'source:invalid|some/file' => 'source:invalid|some/file',
522 'source:invalid|some/file' => 'source:invalid|some/file',
523 }
523 }
524
524
525 @project = Project.find(1)
525 @project = Project.find(1)
526 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
526 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
527 end
527 end
528
528
529 def test_cross_project_multiple_repositories_redmine_links
529 def test_cross_project_multiple_repositories_redmine_links
530 svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn1', :url => 'file:///foo/hg')
530 svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn1', :url => 'file:///foo/hg')
531 Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
531 Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
532 hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
532 hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
533 Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
533 Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
534
534
535 changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
535 changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
536 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
536 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
537 svn_changeset_link = link_to('ecookbook:svn1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn1', :rev => 123},
537 svn_changeset_link = link_to('ecookbook:svn1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn1', :rev => 123},
538 :class => 'changeset', :title => '')
538 :class => 'changeset', :title => '')
539 hg_changeset_link = link_to('ecookbook:hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
539 hg_changeset_link = link_to('ecookbook:hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
540 :class => 'changeset', :title => '')
540 :class => 'changeset', :title => '')
541
541
542 source_link = link_to('ecookbook:source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
542 source_link = link_to('ecookbook:source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
543 hg_source_link = link_to('ecookbook:source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
543 hg_source_link = link_to('ecookbook:source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
544
544
545 to_test = {
545 to_test = {
546 'ecookbook:r2' => changeset_link,
546 'ecookbook:r2' => changeset_link,
547 'ecookbook:svn1|r123' => svn_changeset_link,
547 'ecookbook:svn1|r123' => svn_changeset_link,
548 'ecookbook:invalid|r123' => 'ecookbook:invalid|r123',
548 'ecookbook:invalid|r123' => 'ecookbook:invalid|r123',
549 'ecookbook:commit:hg1|abcd' => hg_changeset_link,
549 'ecookbook:commit:hg1|abcd' => hg_changeset_link,
550 'ecookbook:commit:invalid|abcd' => 'ecookbook:commit:invalid|abcd',
550 'ecookbook:commit:invalid|abcd' => 'ecookbook:commit:invalid|abcd',
551 'invalid:commit:invalid|abcd' => 'invalid:commit:invalid|abcd',
551 'invalid:commit:invalid|abcd' => 'invalid:commit:invalid|abcd',
552 # source
552 # source
553 'ecookbook:source:some/file' => source_link,
553 'ecookbook:source:some/file' => source_link,
554 'ecookbook:source:hg1|some/file' => hg_source_link,
554 'ecookbook:source:hg1|some/file' => hg_source_link,
555 'ecookbook:source:invalid|some/file' => 'ecookbook:source:invalid|some/file',
555 'ecookbook:source:invalid|some/file' => 'ecookbook:source:invalid|some/file',
556 'invalid:source:invalid|some/file' => 'invalid:source:invalid|some/file',
556 'invalid:source:invalid|some/file' => 'invalid:source:invalid|some/file',
557 }
557 }
558
558
559 @project = Project.find(3)
559 @project = Project.find(3)
560 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
560 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
561 end
561 end
562
562
563 def test_redmine_links_git_commit
563 def test_redmine_links_git_commit
564 changeset_link = link_to('abcd',
564 changeset_link = link_to('abcd',
565 {
565 {
566 :controller => 'repositories',
566 :controller => 'repositories',
567 :action => 'revision',
567 :action => 'revision',
568 :id => 'subproject1',
568 :id => 'subproject1',
569 :rev => 'abcd',
569 :rev => 'abcd',
570 },
570 },
571 :class => 'changeset', :title => 'test commit')
571 :class => 'changeset', :title => 'test commit')
572 to_test = {
572 to_test = {
573 'commit:abcd' => changeset_link,
573 'commit:abcd' => changeset_link,
574 }
574 }
575 @project = Project.find(3)
575 @project = Project.find(3)
576 r = Repository::Git.create!(:project => @project, :url => '/tmp/test/git')
576 r = Repository::Git.create!(:project => @project, :url => '/tmp/test/git')
577 assert r
577 assert r
578 c = Changeset.new(:repository => r,
578 c = Changeset.new(:repository => r,
579 :committed_on => Time.now,
579 :committed_on => Time.now,
580 :revision => 'abcd',
580 :revision => 'abcd',
581 :scmid => 'abcd',
581 :scmid => 'abcd',
582 :comments => 'test commit')
582 :comments => 'test commit')
583 assert( c.save )
583 assert( c.save )
584 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
584 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
585 end
585 end
586
586
587 # TODO: Bazaar commit id contains mail address, so it contains '@' and '_'.
587 # TODO: Bazaar commit id contains mail address, so it contains '@' and '_'.
588 def test_redmine_links_darcs_commit
588 def test_redmine_links_darcs_commit
589 changeset_link = link_to('20080308225258-98289-abcd456efg.gz',
589 changeset_link = link_to('20080308225258-98289-abcd456efg.gz',
590 {
590 {
591 :controller => 'repositories',
591 :controller => 'repositories',
592 :action => 'revision',
592 :action => 'revision',
593 :id => 'subproject1',
593 :id => 'subproject1',
594 :rev => '123',
594 :rev => '123',
595 },
595 },
596 :class => 'changeset', :title => 'test commit')
596 :class => 'changeset', :title => 'test commit')
597 to_test = {
597 to_test = {
598 'commit:20080308225258-98289-abcd456efg.gz' => changeset_link,
598 'commit:20080308225258-98289-abcd456efg.gz' => changeset_link,
599 }
599 }
600 @project = Project.find(3)
600 @project = Project.find(3)
601 r = Repository::Darcs.create!(
601 r = Repository::Darcs.create!(
602 :project => @project, :url => '/tmp/test/darcs',
602 :project => @project, :url => '/tmp/test/darcs',
603 :log_encoding => 'UTF-8')
603 :log_encoding => 'UTF-8')
604 assert r
604 assert r
605 c = Changeset.new(:repository => r,
605 c = Changeset.new(:repository => r,
606 :committed_on => Time.now,
606 :committed_on => Time.now,
607 :revision => '123',
607 :revision => '123',
608 :scmid => '20080308225258-98289-abcd456efg.gz',
608 :scmid => '20080308225258-98289-abcd456efg.gz',
609 :comments => 'test commit')
609 :comments => 'test commit')
610 assert( c.save )
610 assert( c.save )
611 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
611 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
612 end
612 end
613
613
614 def test_redmine_links_mercurial_commit
614 def test_redmine_links_mercurial_commit
615 changeset_link_rev = link_to('r123',
615 changeset_link_rev = link_to('r123',
616 {
616 {
617 :controller => 'repositories',
617 :controller => 'repositories',
618 :action => 'revision',
618 :action => 'revision',
619 :id => 'subproject1',
619 :id => 'subproject1',
620 :rev => '123' ,
620 :rev => '123' ,
621 },
621 },
622 :class => 'changeset', :title => 'test commit')
622 :class => 'changeset', :title => 'test commit')
623 changeset_link_commit = link_to('abcd',
623 changeset_link_commit = link_to('abcd',
624 {
624 {
625 :controller => 'repositories',
625 :controller => 'repositories',
626 :action => 'revision',
626 :action => 'revision',
627 :id => 'subproject1',
627 :id => 'subproject1',
628 :rev => 'abcd' ,
628 :rev => 'abcd' ,
629 },
629 },
630 :class => 'changeset', :title => 'test commit')
630 :class => 'changeset', :title => 'test commit')
631 to_test = {
631 to_test = {
632 'r123' => changeset_link_rev,
632 'r123' => changeset_link_rev,
633 'commit:abcd' => changeset_link_commit,
633 'commit:abcd' => changeset_link_commit,
634 }
634 }
635 @project = Project.find(3)
635 @project = Project.find(3)
636 r = Repository::Mercurial.create!(:project => @project, :url => '/tmp/test')
636 r = Repository::Mercurial.create!(:project => @project, :url => '/tmp/test')
637 assert r
637 assert r
638 c = Changeset.new(:repository => r,
638 c = Changeset.new(:repository => r,
639 :committed_on => Time.now,
639 :committed_on => Time.now,
640 :revision => '123',
640 :revision => '123',
641 :scmid => 'abcd',
641 :scmid => 'abcd',
642 :comments => 'test commit')
642 :comments => 'test commit')
643 assert( c.save )
643 assert( c.save )
644 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
644 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
645 end
645 end
646
646
647 def test_attachment_links
647 def test_attachment_links
648 text = 'attachment:error281.txt'
648 text = 'attachment:error281.txt'
649 result = link_to("error281.txt", "/attachments/download/1/error281.txt",
649 result = link_to("error281.txt", "/attachments/download/1/error281.txt",
650 :class => "attachment")
650 :class => "attachment")
651 assert_equal "<p>#{result}</p>",
651 assert_equal "<p>#{result}</p>",
652 textilizable(text,
652 textilizable(text,
653 :attachments => Issue.find(3).attachments),
653 :attachments => Issue.find(3).attachments),
654 "#{text} failed"
654 "#{text} failed"
655 end
655 end
656
656
657 def test_attachment_link_should_link_to_latest_attachment
657 def test_attachment_link_should_link_to_latest_attachment
658 set_tmp_attachments_directory
658 set_tmp_attachments_directory
659 a1 = Attachment.generate!(:filename => "test.txt", :created_on => 1.hour.ago)
659 a1 = Attachment.generate!(:filename => "test.txt", :created_on => 1.hour.ago)
660 a2 = Attachment.generate!(:filename => "test.txt")
660 a2 = Attachment.generate!(:filename => "test.txt")
661 result = link_to("test.txt", "/attachments/download/#{a2.id}/test.txt",
661 result = link_to("test.txt", "/attachments/download/#{a2.id}/test.txt",
662 :class => "attachment")
662 :class => "attachment")
663 assert_equal "<p>#{result}</p>",
663 assert_equal "<p>#{result}</p>",
664 textilizable('attachment:test.txt', :attachments => [a1, a2])
664 textilizable('attachment:test.txt', :attachments => [a1, a2])
665 end
665 end
666
666
667 def test_wiki_links
667 def test_wiki_links
668 russian_eacape = CGI.escape(@russian_test)
668 russian_eacape = CGI.escape(@russian_test)
669 to_test = {
669 to_test = {
670 '[[CookBook documentation]]' =>
670 '[[CookBook documentation]]' =>
671 link_to("CookBook documentation",
671 link_to("CookBook documentation",
672 "/projects/ecookbook/wiki/CookBook_documentation",
672 "/projects/ecookbook/wiki/CookBook_documentation",
673 :class => "wiki-page"),
673 :class => "wiki-page"),
674 '[[Another page|Page]]' =>
674 '[[Another page|Page]]' =>
675 link_to("Page",
675 link_to("Page",
676 "/projects/ecookbook/wiki/Another_page",
676 "/projects/ecookbook/wiki/Another_page",
677 :class => "wiki-page"),
677 :class => "wiki-page"),
678 # title content should be formatted
678 # title content should be formatted
679 '[[Another page|With _styled_ *title*]]' =>
679 '[[Another page|With _styled_ *title*]]' =>
680 link_to("With <em>styled</em> <strong>title</strong>".html_safe,
680 link_to("With <em>styled</em> <strong>title</strong>".html_safe,
681 "/projects/ecookbook/wiki/Another_page",
681 "/projects/ecookbook/wiki/Another_page",
682 :class => "wiki-page"),
682 :class => "wiki-page"),
683 '[[Another page|With title containing <strong>HTML entities &amp; markups</strong>]]' =>
683 '[[Another page|With title containing <strong>HTML entities &amp; markups</strong>]]' =>
684 link_to("With title containing &lt;strong&gt;HTML entities &amp; markups&lt;/strong&gt;".html_safe,
684 link_to("With title containing &lt;strong&gt;HTML entities &amp; markups&lt;/strong&gt;".html_safe,
685 "/projects/ecookbook/wiki/Another_page",
685 "/projects/ecookbook/wiki/Another_page",
686 :class => "wiki-page"),
686 :class => "wiki-page"),
687 # link with anchor
687 # link with anchor
688 '[[CookBook documentation#One-section]]' =>
688 '[[CookBook documentation#One-section]]' =>
689 link_to("CookBook documentation",
689 link_to("CookBook documentation",
690 "/projects/ecookbook/wiki/CookBook_documentation#One-section",
690 "/projects/ecookbook/wiki/CookBook_documentation#One-section",
691 :class => "wiki-page"),
691 :class => "wiki-page"),
692 '[[Another page#anchor|Page]]' =>
692 '[[Another page#anchor|Page]]' =>
693 link_to("Page",
693 link_to("Page",
694 "/projects/ecookbook/wiki/Another_page#anchor",
694 "/projects/ecookbook/wiki/Another_page#anchor",
695 :class => "wiki-page"),
695 :class => "wiki-page"),
696 # UTF8 anchor
696 # UTF8 anchor
697 "[[Another_page##{@russian_test}|#{@russian_test}]]" =>
697 "[[Another_page##{@russian_test}|#{@russian_test}]]" =>
698 link_to(@russian_test,
698 link_to(@russian_test,
699 "/projects/ecookbook/wiki/Another_page##{russian_eacape}",
699 "/projects/ecookbook/wiki/Another_page##{russian_eacape}",
700 :class => "wiki-page"),
700 :class => "wiki-page"),
701 # page that doesn't exist
701 # page that doesn't exist
702 '[[Unknown page]]' =>
702 '[[Unknown page]]' =>
703 link_to("Unknown page",
703 link_to("Unknown page",
704 "/projects/ecookbook/wiki/Unknown_page",
704 "/projects/ecookbook/wiki/Unknown_page",
705 :class => "wiki-page new"),
705 :class => "wiki-page new"),
706 '[[Unknown page|404]]' =>
706 '[[Unknown page|404]]' =>
707 link_to("404",
707 link_to("404",
708 "/projects/ecookbook/wiki/Unknown_page",
708 "/projects/ecookbook/wiki/Unknown_page",
709 :class => "wiki-page new"),
709 :class => "wiki-page new"),
710 # link to another project wiki
710 # link to another project wiki
711 '[[onlinestore:]]' =>
711 '[[onlinestore:]]' =>
712 link_to("onlinestore",
712 link_to("onlinestore",
713 "/projects/onlinestore/wiki",
713 "/projects/onlinestore/wiki",
714 :class => "wiki-page"),
714 :class => "wiki-page"),
715 '[[onlinestore:|Wiki]]' =>
715 '[[onlinestore:|Wiki]]' =>
716 link_to("Wiki",
716 link_to("Wiki",
717 "/projects/onlinestore/wiki",
717 "/projects/onlinestore/wiki",
718 :class => "wiki-page"),
718 :class => "wiki-page"),
719 '[[onlinestore:Start page]]' =>
719 '[[onlinestore:Start page]]' =>
720 link_to("Start page",
720 link_to("Start page",
721 "/projects/onlinestore/wiki/Start_page",
721 "/projects/onlinestore/wiki/Start_page",
722 :class => "wiki-page"),
722 :class => "wiki-page"),
723 '[[onlinestore:Start page|Text]]' =>
723 '[[onlinestore:Start page|Text]]' =>
724 link_to("Text",
724 link_to("Text",
725 "/projects/onlinestore/wiki/Start_page",
725 "/projects/onlinestore/wiki/Start_page",
726 :class => "wiki-page"),
726 :class => "wiki-page"),
727 '[[onlinestore:Unknown page]]' =>
727 '[[onlinestore:Unknown page]]' =>
728 link_to("Unknown page",
728 link_to("Unknown page",
729 "/projects/onlinestore/wiki/Unknown_page",
729 "/projects/onlinestore/wiki/Unknown_page",
730 :class => "wiki-page new"),
730 :class => "wiki-page new"),
731 # struck through link
731 # struck through link
732 '-[[Another page|Page]]-' =>
732 '-[[Another page|Page]]-' =>
733 "<del>".html_safe +
733 "<del>".html_safe +
734 link_to("Page",
734 link_to("Page",
735 "/projects/ecookbook/wiki/Another_page",
735 "/projects/ecookbook/wiki/Another_page",
736 :class => "wiki-page").html_safe +
736 :class => "wiki-page").html_safe +
737 "</del>".html_safe,
737 "</del>".html_safe,
738 '-[[Another page|Page]] link-' =>
738 '-[[Another page|Page]] link-' =>
739 "<del>".html_safe +
739 "<del>".html_safe +
740 link_to("Page",
740 link_to("Page",
741 "/projects/ecookbook/wiki/Another_page",
741 "/projects/ecookbook/wiki/Another_page",
742 :class => "wiki-page").html_safe +
742 :class => "wiki-page").html_safe +
743 " link</del>".html_safe,
743 " link</del>".html_safe,
744 # escaping
744 # escaping
745 '![[Another page|Page]]' => '[[Another page|Page]]',
745 '![[Another page|Page]]' => '[[Another page|Page]]',
746 # project does not exist
746 # project does not exist
747 '[[unknowproject:Start]]' => '[[unknowproject:Start]]',
747 '[[unknowproject:Start]]' => '[[unknowproject:Start]]',
748 '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]',
748 '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]',
749 }
749 }
750 @project = Project.find(1)
750 @project = Project.find(1)
751 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
751 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
752 end
752 end
753
753
754 def test_wiki_links_within_local_file_generation_context
754 def test_wiki_links_within_local_file_generation_context
755 to_test = {
755 to_test = {
756 # link to a page
756 # link to a page
757 '[[CookBook documentation]]' =>
757 '[[CookBook documentation]]' =>
758 link_to("CookBook documentation", "CookBook_documentation.html",
758 link_to("CookBook documentation", "CookBook_documentation.html",
759 :class => "wiki-page"),
759 :class => "wiki-page"),
760 '[[CookBook documentation|documentation]]' =>
760 '[[CookBook documentation|documentation]]' =>
761 link_to("documentation", "CookBook_documentation.html",
761 link_to("documentation", "CookBook_documentation.html",
762 :class => "wiki-page"),
762 :class => "wiki-page"),
763 '[[CookBook documentation#One-section]]' =>
763 '[[CookBook documentation#One-section]]' =>
764 link_to("CookBook documentation", "CookBook_documentation.html#One-section",
764 link_to("CookBook documentation", "CookBook_documentation.html#One-section",
765 :class => "wiki-page"),
765 :class => "wiki-page"),
766 '[[CookBook documentation#One-section|documentation]]' =>
766 '[[CookBook documentation#One-section|documentation]]' =>
767 link_to("documentation", "CookBook_documentation.html#One-section",
767 link_to("documentation", "CookBook_documentation.html#One-section",
768 :class => "wiki-page"),
768 :class => "wiki-page"),
769 # page that doesn't exist
769 # page that doesn't exist
770 '[[Unknown page]]' =>
770 '[[Unknown page]]' =>
771 link_to("Unknown page", "Unknown_page.html",
771 link_to("Unknown page", "Unknown_page.html",
772 :class => "wiki-page new"),
772 :class => "wiki-page new"),
773 '[[Unknown page|404]]' =>
773 '[[Unknown page|404]]' =>
774 link_to("404", "Unknown_page.html",
774 link_to("404", "Unknown_page.html",
775 :class => "wiki-page new"),
775 :class => "wiki-page new"),
776 '[[Unknown page#anchor]]' =>
776 '[[Unknown page#anchor]]' =>
777 link_to("Unknown page", "Unknown_page.html#anchor",
777 link_to("Unknown page", "Unknown_page.html#anchor",
778 :class => "wiki-page new"),
778 :class => "wiki-page new"),
779 '[[Unknown page#anchor|404]]' =>
779 '[[Unknown page#anchor|404]]' =>
780 link_to("404", "Unknown_page.html#anchor",
780 link_to("404", "Unknown_page.html#anchor",
781 :class => "wiki-page new"),
781 :class => "wiki-page new"),
782 }
782 }
783 @project = Project.find(1)
783 @project = Project.find(1)
784 to_test.each do |text, result|
784 to_test.each do |text, result|
785 assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :local)
785 assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :local)
786 end
786 end
787 end
787 end
788
788
789 def test_wiki_links_within_wiki_page_context
789 def test_wiki_links_within_wiki_page_context
790 page = WikiPage.find_by_title('Another_page' )
790 page = WikiPage.find_by_title('Another_page' )
791 to_test = {
791 to_test = {
792 '[[CookBook documentation]]' =>
792 '[[CookBook documentation]]' =>
793 link_to("CookBook documentation",
793 link_to("CookBook documentation",
794 "/projects/ecookbook/wiki/CookBook_documentation",
794 "/projects/ecookbook/wiki/CookBook_documentation",
795 :class => "wiki-page"),
795 :class => "wiki-page"),
796 '[[CookBook documentation|documentation]]' =>
796 '[[CookBook documentation|documentation]]' =>
797 link_to("documentation",
797 link_to("documentation",
798 "/projects/ecookbook/wiki/CookBook_documentation",
798 "/projects/ecookbook/wiki/CookBook_documentation",
799 :class => "wiki-page"),
799 :class => "wiki-page"),
800 '[[CookBook documentation#One-section]]' =>
800 '[[CookBook documentation#One-section]]' =>
801 link_to("CookBook documentation",
801 link_to("CookBook documentation",
802 "/projects/ecookbook/wiki/CookBook_documentation#One-section",
802 "/projects/ecookbook/wiki/CookBook_documentation#One-section",
803 :class => "wiki-page"),
803 :class => "wiki-page"),
804 '[[CookBook documentation#One-section|documentation]]' =>
804 '[[CookBook documentation#One-section|documentation]]' =>
805 link_to("documentation",
805 link_to("documentation",
806 "/projects/ecookbook/wiki/CookBook_documentation#One-section",
806 "/projects/ecookbook/wiki/CookBook_documentation#One-section",
807 :class => "wiki-page"),
807 :class => "wiki-page"),
808 # link to the current page
808 # link to the current page
809 '[[Another page]]' =>
809 '[[Another page]]' =>
810 link_to("Another page",
810 link_to("Another page",
811 "/projects/ecookbook/wiki/Another_page",
811 "/projects/ecookbook/wiki/Another_page",
812 :class => "wiki-page"),
812 :class => "wiki-page"),
813 '[[Another page|Page]]' =>
813 '[[Another page|Page]]' =>
814 link_to("Page",
814 link_to("Page",
815 "/projects/ecookbook/wiki/Another_page",
815 "/projects/ecookbook/wiki/Another_page",
816 :class => "wiki-page"),
816 :class => "wiki-page"),
817 '[[Another page#anchor]]' =>
817 '[[Another page#anchor]]' =>
818 link_to("Another page",
818 link_to("Another page",
819 "#anchor",
819 "#anchor",
820 :class => "wiki-page"),
820 :class => "wiki-page"),
821 '[[Another page#anchor|Page]]' =>
821 '[[Another page#anchor|Page]]' =>
822 link_to("Page",
822 link_to("Page",
823 "#anchor",
823 "#anchor",
824 :class => "wiki-page"),
824 :class => "wiki-page"),
825 # page that doesn't exist
825 # page that doesn't exist
826 '[[Unknown page]]' =>
826 '[[Unknown page]]' =>
827 link_to("Unknown page",
827 link_to("Unknown page",
828 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page",
828 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page",
829 :class => "wiki-page new"),
829 :class => "wiki-page new"),
830 '[[Unknown page|404]]' =>
830 '[[Unknown page|404]]' =>
831 link_to("404",
831 link_to("404",
832 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page",
832 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page",
833 :class => "wiki-page new"),
833 :class => "wiki-page new"),
834 '[[Unknown page#anchor]]' =>
834 '[[Unknown page#anchor]]' =>
835 link_to("Unknown page",
835 link_to("Unknown page",
836 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor",
836 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor",
837 :class => "wiki-page new"),
837 :class => "wiki-page new"),
838 '[[Unknown page#anchor|404]]' =>
838 '[[Unknown page#anchor|404]]' =>
839 link_to("404",
839 link_to("404",
840 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor",
840 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor",
841 :class => "wiki-page new"),
841 :class => "wiki-page new"),
842 }
842 }
843 @project = Project.find(1)
843 @project = Project.find(1)
844 to_test.each do |text, result|
844 to_test.each do |text, result|
845 assert_equal "<p>#{result}</p>",
845 assert_equal "<p>#{result}</p>",
846 textilizable(WikiContent.new( :text => text, :page => page ), :text)
846 textilizable(WikiContent.new( :text => text, :page => page ), :text)
847 end
847 end
848 end
848 end
849
849
850 def test_wiki_links_anchor_option_should_prepend_page_title_to_href
850 def test_wiki_links_anchor_option_should_prepend_page_title_to_href
851 to_test = {
851 to_test = {
852 # link to a page
852 # link to a page
853 '[[CookBook documentation]]' =>
853 '[[CookBook documentation]]' =>
854 link_to("CookBook documentation",
854 link_to("CookBook documentation",
855 "#CookBook_documentation",
855 "#CookBook_documentation",
856 :class => "wiki-page"),
856 :class => "wiki-page"),
857 '[[CookBook documentation|documentation]]' =>
857 '[[CookBook documentation|documentation]]' =>
858 link_to("documentation",
858 link_to("documentation",
859 "#CookBook_documentation",
859 "#CookBook_documentation",
860 :class => "wiki-page"),
860 :class => "wiki-page"),
861 '[[CookBook documentation#One-section]]' =>
861 '[[CookBook documentation#One-section]]' =>
862 link_to("CookBook documentation",
862 link_to("CookBook documentation",
863 "#CookBook_documentation_One-section",
863 "#CookBook_documentation_One-section",
864 :class => "wiki-page"),
864 :class => "wiki-page"),
865 '[[CookBook documentation#One-section|documentation]]' =>
865 '[[CookBook documentation#One-section|documentation]]' =>
866 link_to("documentation",
866 link_to("documentation",
867 "#CookBook_documentation_One-section",
867 "#CookBook_documentation_One-section",
868 :class => "wiki-page"),
868 :class => "wiki-page"),
869 # page that doesn't exist
869 # page that doesn't exist
870 '[[Unknown page]]' =>
870 '[[Unknown page]]' =>
871 link_to("Unknown page",
871 link_to("Unknown page",
872 "#Unknown_page",
872 "#Unknown_page",
873 :class => "wiki-page new"),
873 :class => "wiki-page new"),
874 '[[Unknown page|404]]' =>
874 '[[Unknown page|404]]' =>
875 link_to("404",
875 link_to("404",
876 "#Unknown_page",
876 "#Unknown_page",
877 :class => "wiki-page new"),
877 :class => "wiki-page new"),
878 '[[Unknown page#anchor]]' =>
878 '[[Unknown page#anchor]]' =>
879 link_to("Unknown page",
879 link_to("Unknown page",
880 "#Unknown_page_anchor",
880 "#Unknown_page_anchor",
881 :class => "wiki-page new"),
881 :class => "wiki-page new"),
882 '[[Unknown page#anchor|404]]' =>
882 '[[Unknown page#anchor|404]]' =>
883 link_to("404",
883 link_to("404",
884 "#Unknown_page_anchor",
884 "#Unknown_page_anchor",
885 :class => "wiki-page new"),
885 :class => "wiki-page new"),
886 }
886 }
887 @project = Project.find(1)
887 @project = Project.find(1)
888 to_test.each do |text, result|
888 to_test.each do |text, result|
889 assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :anchor)
889 assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :anchor)
890 end
890 end
891 end
891 end
892
892
893 def test_html_tags
893 def test_html_tags
894 to_test = {
894 to_test = {
895 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
895 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
896 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
896 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
897 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
897 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
898 # do not escape pre/code tags
898 # do not escape pre/code tags
899 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
899 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
900 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
900 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
901 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
901 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
902 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
902 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
903 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
903 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
904 # remove attributes except class
904 # remove attributes except class
905 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
905 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
906 '<pre class="foo">some text</pre>' => '<pre class="foo">some text</pre>',
906 '<pre class="foo">some text</pre>' => '<pre class="foo">some text</pre>',
907 "<pre class='foo bar'>some text</pre>" => "<pre class='foo bar'>some text</pre>",
907 "<pre class='foo bar'>some text</pre>" => "<pre class='foo bar'>some text</pre>",
908 '<pre class="foo bar">some text</pre>' => '<pre class="foo bar">some text</pre>',
908 '<pre class="foo bar">some text</pre>' => '<pre class="foo bar">some text</pre>',
909 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
909 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
910 # xss
910 # xss
911 '<pre><code class=""onmouseover="alert(1)">text</code></pre>' => '<pre><code>text</code></pre>',
911 '<pre><code class=""onmouseover="alert(1)">text</code></pre>' => '<pre><code>text</code></pre>',
912 '<pre class=""onmouseover="alert(1)">text</pre>' => '<pre>text</pre>',
912 '<pre class=""onmouseover="alert(1)">text</pre>' => '<pre>text</pre>',
913 }
913 }
914 to_test.each { |text, result| assert_equal result, textilizable(text) }
914 to_test.each { |text, result| assert_equal result, textilizable(text) }
915 end
915 end
916
916
917 def test_allowed_html_tags
917 def test_allowed_html_tags
918 to_test = {
918 to_test = {
919 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
919 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
920 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
920 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
921 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
921 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
922 }
922 }
923 to_test.each { |text, result| assert_equal result, textilizable(text) }
923 to_test.each { |text, result| assert_equal result, textilizable(text) }
924 end
924 end
925
925
926 def test_pre_tags
926 def test_pre_tags
927 raw = <<-RAW
927 raw = <<-RAW
928 Before
928 Before
929
929
930 <pre>
930 <pre>
931 <prepared-statement-cache-size>32</prepared-statement-cache-size>
931 <prepared-statement-cache-size>32</prepared-statement-cache-size>
932 </pre>
932 </pre>
933
933
934 After
934 After
935 RAW
935 RAW
936
936
937 expected = <<-EXPECTED
937 expected = <<-EXPECTED
938 <p>Before</p>
938 <p>Before</p>
939 <pre>
939 <pre>
940 &lt;prepared-statement-cache-size&gt;32&lt;/prepared-statement-cache-size&gt;
940 &lt;prepared-statement-cache-size&gt;32&lt;/prepared-statement-cache-size&gt;
941 </pre>
941 </pre>
942 <p>After</p>
942 <p>After</p>
943 EXPECTED
943 EXPECTED
944
944
945 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
945 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
946 end
946 end
947
947
948 def test_pre_content_should_not_parse_wiki_and_redmine_links
948 def test_pre_content_should_not_parse_wiki_and_redmine_links
949 raw = <<-RAW
949 raw = <<-RAW
950 [[CookBook documentation]]
950 [[CookBook documentation]]
951
951
952 #1
952 #1
953
953
954 <pre>
954 <pre>
955 [[CookBook documentation]]
955 [[CookBook documentation]]
956
956
957 #1
957 #1
958 </pre>
958 </pre>
959 RAW
959 RAW
960
960
961 result1 = link_to("CookBook documentation",
961 result1 = link_to("CookBook documentation",
962 "/projects/ecookbook/wiki/CookBook_documentation",
962 "/projects/ecookbook/wiki/CookBook_documentation",
963 :class => "wiki-page")
963 :class => "wiki-page")
964 result2 = link_to('#1',
964 result2 = link_to('#1',
965 "/issues/1",
965 "/issues/1",
966 :class => Issue.find(1).css_classes,
966 :class => Issue.find(1).css_classes,
967 :title => "Cannot print recipes (New)")
967 :title => "Bug: Cannot print recipes (New)")
968
968
969 expected = <<-EXPECTED
969 expected = <<-EXPECTED
970 <p>#{result1}</p>
970 <p>#{result1}</p>
971 <p>#{result2}</p>
971 <p>#{result2}</p>
972 <pre>
972 <pre>
973 [[CookBook documentation]]
973 [[CookBook documentation]]
974
974
975 #1
975 #1
976 </pre>
976 </pre>
977 EXPECTED
977 EXPECTED
978
978
979 @project = Project.find(1)
979 @project = Project.find(1)
980 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
980 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
981 end
981 end
982
982
983 def test_non_closing_pre_blocks_should_be_closed
983 def test_non_closing_pre_blocks_should_be_closed
984 raw = <<-RAW
984 raw = <<-RAW
985 <pre><code>
985 <pre><code>
986 RAW
986 RAW
987
987
988 expected = <<-EXPECTED
988 expected = <<-EXPECTED
989 <pre><code>
989 <pre><code>
990 </code></pre>
990 </code></pre>
991 EXPECTED
991 EXPECTED
992
992
993 @project = Project.find(1)
993 @project = Project.find(1)
994 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
994 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
995 end
995 end
996
996
997 def test_syntax_highlight
997 def test_syntax_highlight
998 raw = <<-RAW
998 raw = <<-RAW
999 <pre><code class="ruby">
999 <pre><code class="ruby">
1000 # Some ruby code here
1000 # Some ruby code here
1001 </code></pre>
1001 </code></pre>
1002 RAW
1002 RAW
1003
1003
1004 expected = <<-EXPECTED
1004 expected = <<-EXPECTED
1005 <pre><code class="ruby syntaxhl"><span class=\"CodeRay\"><span class="comment"># Some ruby code here</span></span>
1005 <pre><code class="ruby syntaxhl"><span class=\"CodeRay\"><span class="comment"># Some ruby code here</span></span>
1006 </code></pre>
1006 </code></pre>
1007 EXPECTED
1007 EXPECTED
1008
1008
1009 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
1009 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
1010 end
1010 end
1011
1011
1012 def test_to_path_param
1012 def test_to_path_param
1013 assert_equal 'test1/test2', to_path_param('test1/test2')
1013 assert_equal 'test1/test2', to_path_param('test1/test2')
1014 assert_equal 'test1/test2', to_path_param('/test1/test2/')
1014 assert_equal 'test1/test2', to_path_param('/test1/test2/')
1015 assert_equal 'test1/test2', to_path_param('//test1/test2/')
1015 assert_equal 'test1/test2', to_path_param('//test1/test2/')
1016 assert_equal nil, to_path_param('/')
1016 assert_equal nil, to_path_param('/')
1017 end
1017 end
1018
1018
1019 def test_wiki_links_in_tables
1019 def test_wiki_links_in_tables
1020 text = "|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|"
1020 text = "|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|"
1021 link1 = link_to("Link title", "/projects/ecookbook/wiki/Page", :class => "wiki-page new")
1021 link1 = link_to("Link title", "/projects/ecookbook/wiki/Page", :class => "wiki-page new")
1022 link2 = link_to("Other title", "/projects/ecookbook/wiki/Other_Page", :class => "wiki-page new")
1022 link2 = link_to("Other title", "/projects/ecookbook/wiki/Other_Page", :class => "wiki-page new")
1023 link3 = link_to("Last page", "/projects/ecookbook/wiki/Last_page", :class => "wiki-page new")
1023 link3 = link_to("Last page", "/projects/ecookbook/wiki/Last_page", :class => "wiki-page new")
1024 result = "<tr><td>#{link1}</td>" +
1024 result = "<tr><td>#{link1}</td>" +
1025 "<td>#{link2}</td>" +
1025 "<td>#{link2}</td>" +
1026 "</tr><tr><td>Cell 21</td><td>#{link3}</td></tr>"
1026 "</tr><tr><td>Cell 21</td><td>#{link3}</td></tr>"
1027 @project = Project.find(1)
1027 @project = Project.find(1)
1028 assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '')
1028 assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '')
1029 end
1029 end
1030
1030
1031 def test_text_formatting
1031 def test_text_formatting
1032 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
1032 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
1033 '(_text within parentheses_)' => '(<em>text within parentheses</em>)',
1033 '(_text within parentheses_)' => '(<em>text within parentheses</em>)',
1034 'a *Humane Web* Text Generator' => 'a <strong>Humane Web</strong> Text Generator',
1034 'a *Humane Web* Text Generator' => 'a <strong>Humane Web</strong> Text Generator',
1035 'a H *umane* W *eb* T *ext* G *enerator*' => 'a H <strong>umane</strong> W <strong>eb</strong> T <strong>ext</strong> G <strong>enerator</strong>',
1035 'a H *umane* W *eb* T *ext* G *enerator*' => 'a H <strong>umane</strong> W <strong>eb</strong> T <strong>ext</strong> G <strong>enerator</strong>',
1036 'a *H* umane *W* eb *T* ext *G* enerator' => 'a <strong>H</strong> umane <strong>W</strong> eb <strong>T</strong> ext <strong>G</strong> enerator',
1036 'a *H* umane *W* eb *T* ext *G* enerator' => 'a <strong>H</strong> umane <strong>W</strong> eb <strong>T</strong> ext <strong>G</strong> enerator',
1037 }
1037 }
1038 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
1038 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
1039 end
1039 end
1040
1040
1041 def test_wiki_horizontal_rule
1041 def test_wiki_horizontal_rule
1042 assert_equal '<hr />', textilizable('---')
1042 assert_equal '<hr />', textilizable('---')
1043 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
1043 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
1044 end
1044 end
1045
1045
1046 def test_footnotes
1046 def test_footnotes
1047 raw = <<-RAW
1047 raw = <<-RAW
1048 This is some text[1].
1048 This is some text[1].
1049
1049
1050 fn1. This is the foot note
1050 fn1. This is the foot note
1051 RAW
1051 RAW
1052
1052
1053 expected = <<-EXPECTED
1053 expected = <<-EXPECTED
1054 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
1054 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
1055 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
1055 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
1056 EXPECTED
1056 EXPECTED
1057
1057
1058 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
1058 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
1059 end
1059 end
1060
1060
1061 def test_headings
1061 def test_headings
1062 raw = 'h1. Some heading'
1062 raw = 'h1. Some heading'
1063 expected = %|<a name="Some-heading"></a>\n<h1 >Some heading<a href="#Some-heading" class="wiki-anchor">&para;</a></h1>|
1063 expected = %|<a name="Some-heading"></a>\n<h1 >Some heading<a href="#Some-heading" class="wiki-anchor">&para;</a></h1>|
1064
1064
1065 assert_equal expected, textilizable(raw)
1065 assert_equal expected, textilizable(raw)
1066 end
1066 end
1067
1067
1068 def test_headings_with_special_chars
1068 def test_headings_with_special_chars
1069 # This test makes sure that the generated anchor names match the expected
1069 # This test makes sure that the generated anchor names match the expected
1070 # ones even if the heading text contains unconventional characters
1070 # ones even if the heading text contains unconventional characters
1071 raw = 'h1. Some heading related to version 0.5'
1071 raw = 'h1. Some heading related to version 0.5'
1072 anchor = sanitize_anchor_name("Some-heading-related-to-version-0.5")
1072 anchor = sanitize_anchor_name("Some-heading-related-to-version-0.5")
1073 expected = %|<a name="#{anchor}"></a>\n<h1 >Some heading related to version 0.5<a href="##{anchor}" class="wiki-anchor">&para;</a></h1>|
1073 expected = %|<a name="#{anchor}"></a>\n<h1 >Some heading related to version 0.5<a href="##{anchor}" class="wiki-anchor">&para;</a></h1>|
1074
1074
1075 assert_equal expected, textilizable(raw)
1075 assert_equal expected, textilizable(raw)
1076 end
1076 end
1077
1077
1078 def test_headings_in_wiki_single_page_export_should_be_prepended_with_page_title
1078 def test_headings_in_wiki_single_page_export_should_be_prepended_with_page_title
1079 page = WikiPage.new( :title => 'Page Title', :wiki_id => 1 )
1079 page = WikiPage.new( :title => 'Page Title', :wiki_id => 1 )
1080 content = WikiContent.new( :text => 'h1. Some heading', :page => page )
1080 content = WikiContent.new( :text => 'h1. Some heading', :page => page )
1081
1081
1082 expected = %|<a name="Page_Title_Some-heading"></a>\n<h1 >Some heading<a href="#Page_Title_Some-heading" class="wiki-anchor">&para;</a></h1>|
1082 expected = %|<a name="Page_Title_Some-heading"></a>\n<h1 >Some heading<a href="#Page_Title_Some-heading" class="wiki-anchor">&para;</a></h1>|
1083
1083
1084 assert_equal expected, textilizable(content, :text, :wiki_links => :anchor )
1084 assert_equal expected, textilizable(content, :text, :wiki_links => :anchor )
1085 end
1085 end
1086
1086
1087 def test_table_of_content
1087 def test_table_of_content
1088 raw = <<-RAW
1088 raw = <<-RAW
1089 {{toc}}
1089 {{toc}}
1090
1090
1091 h1. Title
1091 h1. Title
1092
1092
1093 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
1093 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
1094
1094
1095 h2. Subtitle with a [[Wiki]] link
1095 h2. Subtitle with a [[Wiki]] link
1096
1096
1097 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
1097 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
1098
1098
1099 h2. Subtitle with [[Wiki|another Wiki]] link
1099 h2. Subtitle with [[Wiki|another Wiki]] link
1100
1100
1101 h2. Subtitle with %{color:red}red text%
1101 h2. Subtitle with %{color:red}red text%
1102
1102
1103 <pre>
1103 <pre>
1104 some code
1104 some code
1105 </pre>
1105 </pre>
1106
1106
1107 h3. Subtitle with *some* _modifiers_
1107 h3. Subtitle with *some* _modifiers_
1108
1108
1109 h3. Subtitle with @inline code@
1109 h3. Subtitle with @inline code@
1110
1110
1111 h1. Another title
1111 h1. Another title
1112
1112
1113 h3. An "Internet link":http://www.redmine.org/ inside subtitle
1113 h3. An "Internet link":http://www.redmine.org/ inside subtitle
1114
1114
1115 h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
1115 h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
1116
1116
1117 RAW
1117 RAW
1118
1118
1119 expected = '<ul class="toc">' +
1119 expected = '<ul class="toc">' +
1120 '<li><a href="#Title">Title</a>' +
1120 '<li><a href="#Title">Title</a>' +
1121 '<ul>' +
1121 '<ul>' +
1122 '<li><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
1122 '<li><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
1123 '<li><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
1123 '<li><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
1124 '<li><a href="#Subtitle-with-red-text">Subtitle with red text</a>' +
1124 '<li><a href="#Subtitle-with-red-text">Subtitle with red text</a>' +
1125 '<ul>' +
1125 '<ul>' +
1126 '<li><a href="#Subtitle-with-some-modifiers">Subtitle with some modifiers</a></li>' +
1126 '<li><a href="#Subtitle-with-some-modifiers">Subtitle with some modifiers</a></li>' +
1127 '<li><a href="#Subtitle-with-inline-code">Subtitle with inline code</a></li>' +
1127 '<li><a href="#Subtitle-with-inline-code">Subtitle with inline code</a></li>' +
1128 '</ul>' +
1128 '</ul>' +
1129 '</li>' +
1129 '</li>' +
1130 '</ul>' +
1130 '</ul>' +
1131 '</li>' +
1131 '</li>' +
1132 '<li><a href="#Another-title">Another title</a>' +
1132 '<li><a href="#Another-title">Another title</a>' +
1133 '<ul>' +
1133 '<ul>' +
1134 '<li>' +
1134 '<li>' +
1135 '<ul>' +
1135 '<ul>' +
1136 '<li><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
1136 '<li><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
1137 '</ul>' +
1137 '</ul>' +
1138 '</li>' +
1138 '</li>' +
1139 '<li><a href="#Project-Name">Project Name</a></li>' +
1139 '<li><a href="#Project-Name">Project Name</a></li>' +
1140 '</ul>' +
1140 '</ul>' +
1141 '</li>' +
1141 '</li>' +
1142 '</ul>'
1142 '</ul>'
1143
1143
1144 @project = Project.find(1)
1144 @project = Project.find(1)
1145 assert textilizable(raw).gsub("\n", "").include?(expected)
1145 assert textilizable(raw).gsub("\n", "").include?(expected)
1146 end
1146 end
1147
1147
1148 def test_table_of_content_should_generate_unique_anchors
1148 def test_table_of_content_should_generate_unique_anchors
1149 raw = <<-RAW
1149 raw = <<-RAW
1150 {{toc}}
1150 {{toc}}
1151
1151
1152 h1. Title
1152 h1. Title
1153
1153
1154 h2. Subtitle
1154 h2. Subtitle
1155
1155
1156 h2. Subtitle
1156 h2. Subtitle
1157 RAW
1157 RAW
1158
1158
1159 expected = '<ul class="toc">' +
1159 expected = '<ul class="toc">' +
1160 '<li><a href="#Title">Title</a>' +
1160 '<li><a href="#Title">Title</a>' +
1161 '<ul>' +
1161 '<ul>' +
1162 '<li><a href="#Subtitle">Subtitle</a></li>' +
1162 '<li><a href="#Subtitle">Subtitle</a></li>' +
1163 '<li><a href="#Subtitle-2">Subtitle</a></li>'
1163 '<li><a href="#Subtitle-2">Subtitle</a></li>'
1164 '</ul>'
1164 '</ul>'
1165 '</li>' +
1165 '</li>' +
1166 '</ul>'
1166 '</ul>'
1167
1167
1168 @project = Project.find(1)
1168 @project = Project.find(1)
1169 result = textilizable(raw).gsub("\n", "")
1169 result = textilizable(raw).gsub("\n", "")
1170 assert_include expected, result
1170 assert_include expected, result
1171 assert_include '<a name="Subtitle">', result
1171 assert_include '<a name="Subtitle">', result
1172 assert_include '<a name="Subtitle-2">', result
1172 assert_include '<a name="Subtitle-2">', result
1173 end
1173 end
1174
1174
1175 def test_table_of_content_should_contain_included_page_headings
1175 def test_table_of_content_should_contain_included_page_headings
1176 raw = <<-RAW
1176 raw = <<-RAW
1177 {{toc}}
1177 {{toc}}
1178
1178
1179 h1. Included
1179 h1. Included
1180
1180
1181 {{include(Child_1)}}
1181 {{include(Child_1)}}
1182 RAW
1182 RAW
1183
1183
1184 expected = '<ul class="toc">' +
1184 expected = '<ul class="toc">' +
1185 '<li><a href="#Included">Included</a></li>' +
1185 '<li><a href="#Included">Included</a></li>' +
1186 '<li><a href="#Child-page-1">Child page 1</a></li>' +
1186 '<li><a href="#Child-page-1">Child page 1</a></li>' +
1187 '</ul>'
1187 '</ul>'
1188
1188
1189 @project = Project.find(1)
1189 @project = Project.find(1)
1190 assert textilizable(raw).gsub("\n", "").include?(expected)
1190 assert textilizable(raw).gsub("\n", "").include?(expected)
1191 end
1191 end
1192
1192
1193 def test_toc_with_textile_formatting_should_be_parsed
1193 def test_toc_with_textile_formatting_should_be_parsed
1194 with_settings :text_formatting => 'textile' do
1194 with_settings :text_formatting => 'textile' do
1195 assert_select_in textilizable("{{toc}}\n\nh1. Heading"), 'ul.toc li', :text => 'Heading'
1195 assert_select_in textilizable("{{toc}}\n\nh1. Heading"), 'ul.toc li', :text => 'Heading'
1196 assert_select_in textilizable("{{<toc}}\n\nh1. Heading"), 'ul.toc.left li', :text => 'Heading'
1196 assert_select_in textilizable("{{<toc}}\n\nh1. Heading"), 'ul.toc.left li', :text => 'Heading'
1197 assert_select_in textilizable("{{>toc}}\n\nh1. Heading"), 'ul.toc.right li', :text => 'Heading'
1197 assert_select_in textilizable("{{>toc}}\n\nh1. Heading"), 'ul.toc.right li', :text => 'Heading'
1198 end
1198 end
1199 end
1199 end
1200
1200
1201 if Object.const_defined?(:Redcarpet)
1201 if Object.const_defined?(:Redcarpet)
1202 def test_toc_with_markdown_formatting_should_be_parsed
1202 def test_toc_with_markdown_formatting_should_be_parsed
1203 with_settings :text_formatting => 'markdown' do
1203 with_settings :text_formatting => 'markdown' do
1204 assert_select_in textilizable("{{toc}}\n\n# Heading"), 'ul.toc li', :text => 'Heading'
1204 assert_select_in textilizable("{{toc}}\n\n# Heading"), 'ul.toc li', :text => 'Heading'
1205 assert_select_in textilizable("{{<toc}}\n\n# Heading"), 'ul.toc.left li', :text => 'Heading'
1205 assert_select_in textilizable("{{<toc}}\n\n# Heading"), 'ul.toc.left li', :text => 'Heading'
1206 assert_select_in textilizable("{{>toc}}\n\n# Heading"), 'ul.toc.right li', :text => 'Heading'
1206 assert_select_in textilizable("{{>toc}}\n\n# Heading"), 'ul.toc.right li', :text => 'Heading'
1207 end
1207 end
1208 end
1208 end
1209 end
1209 end
1210
1210
1211 def test_section_edit_links
1211 def test_section_edit_links
1212 raw = <<-RAW
1212 raw = <<-RAW
1213 h1. Title
1213 h1. Title
1214
1214
1215 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
1215 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
1216
1216
1217 h2. Subtitle with a [[Wiki]] link
1217 h2. Subtitle with a [[Wiki]] link
1218
1218
1219 h2. Subtitle with *some* _modifiers_
1219 h2. Subtitle with *some* _modifiers_
1220
1220
1221 h2. Subtitle with @inline code@
1221 h2. Subtitle with @inline code@
1222
1222
1223 <pre>
1223 <pre>
1224 some code
1224 some code
1225
1225
1226 h2. heading inside pre
1226 h2. heading inside pre
1227
1227
1228 <h2>html heading inside pre</h2>
1228 <h2>html heading inside pre</h2>
1229 </pre>
1229 </pre>
1230
1230
1231 h2. Subtitle after pre tag
1231 h2. Subtitle after pre tag
1232 RAW
1232 RAW
1233
1233
1234 @project = Project.find(1)
1234 @project = Project.find(1)
1235 set_language_if_valid 'en'
1235 set_language_if_valid 'en'
1236 result = textilizable(raw, :edit_section_links => {:controller => 'wiki', :action => 'edit', :project_id => '1', :id => 'Test'}).gsub("\n", "")
1236 result = textilizable(raw, :edit_section_links => {:controller => 'wiki', :action => 'edit', :project_id => '1', :id => 'Test'}).gsub("\n", "")
1237
1237
1238 # heading that contains inline code
1238 # heading that contains inline code
1239 assert_match Regexp.new('<div class="contextual" title="Edit this section" id="section-4">' +
1239 assert_match Regexp.new('<div class="contextual" title="Edit this section" id="section-4">' +
1240 '<a href="/projects/1/wiki/Test/edit\?section=4"><img src="/images/edit.png(\?\d+)?" alt="Edit" /></a></div>' +
1240 '<a href="/projects/1/wiki/Test/edit\?section=4"><img src="/images/edit.png(\?\d+)?" alt="Edit" /></a></div>' +
1241 '<a name="Subtitle-with-inline-code"></a>' +
1241 '<a name="Subtitle-with-inline-code"></a>' +
1242 '<h2 >Subtitle with <code>inline code</code><a href="#Subtitle-with-inline-code" class="wiki-anchor">&para;</a></h2>'),
1242 '<h2 >Subtitle with <code>inline code</code><a href="#Subtitle-with-inline-code" class="wiki-anchor">&para;</a></h2>'),
1243 result
1243 result
1244
1244
1245 # last heading
1245 # last heading
1246 assert_match Regexp.new('<div class="contextual" title="Edit this section" id="section-5">' +
1246 assert_match Regexp.new('<div class="contextual" title="Edit this section" id="section-5">' +
1247 '<a href="/projects/1/wiki/Test/edit\?section=5"><img src="/images/edit.png(\?\d+)?" alt="Edit" /></a></div>' +
1247 '<a href="/projects/1/wiki/Test/edit\?section=5"><img src="/images/edit.png(\?\d+)?" alt="Edit" /></a></div>' +
1248 '<a name="Subtitle-after-pre-tag"></a>' +
1248 '<a name="Subtitle-after-pre-tag"></a>' +
1249 '<h2 >Subtitle after pre tag<a href="#Subtitle-after-pre-tag" class="wiki-anchor">&para;</a></h2>'),
1249 '<h2 >Subtitle after pre tag<a href="#Subtitle-after-pre-tag" class="wiki-anchor">&para;</a></h2>'),
1250 result
1250 result
1251 end
1251 end
1252
1252
1253 def test_default_formatter
1253 def test_default_formatter
1254 with_settings :text_formatting => 'unknown' do
1254 with_settings :text_formatting => 'unknown' do
1255 text = 'a *link*: http://www.example.net/'
1255 text = 'a *link*: http://www.example.net/'
1256 assert_equal '<p>a *link*: <a class="external" href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
1256 assert_equal '<p>a *link*: <a class="external" href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
1257 end
1257 end
1258 end
1258 end
1259
1259
1260 def test_due_date_distance_in_words
1260 def test_due_date_distance_in_words
1261 to_test = { Date.today => 'Due in 0 days',
1261 to_test = { Date.today => 'Due in 0 days',
1262 Date.today + 1 => 'Due in 1 day',
1262 Date.today + 1 => 'Due in 1 day',
1263 Date.today + 100 => 'Due in about 3 months',
1263 Date.today + 100 => 'Due in about 3 months',
1264 Date.today + 20000 => 'Due in over 54 years',
1264 Date.today + 20000 => 'Due in over 54 years',
1265 Date.today - 1 => '1 day late',
1265 Date.today - 1 => '1 day late',
1266 Date.today - 100 => 'about 3 months late',
1266 Date.today - 100 => 'about 3 months late',
1267 Date.today - 20000 => 'over 54 years late',
1267 Date.today - 20000 => 'over 54 years late',
1268 }
1268 }
1269 ::I18n.locale = :en
1269 ::I18n.locale = :en
1270 to_test.each do |date, expected|
1270 to_test.each do |date, expected|
1271 assert_equal expected, due_date_distance_in_words(date)
1271 assert_equal expected, due_date_distance_in_words(date)
1272 end
1272 end
1273 end
1273 end
1274
1274
1275 def test_avatar_enabled
1275 def test_avatar_enabled
1276 with_settings :gravatar_enabled => '1' do
1276 with_settings :gravatar_enabled => '1' do
1277 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
1277 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
1278 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
1278 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
1279 # Default size is 50
1279 # Default size is 50
1280 assert avatar('jsmith <jsmith@somenet.foo>').include?('size=50')
1280 assert avatar('jsmith <jsmith@somenet.foo>').include?('size=50')
1281 assert avatar('jsmith <jsmith@somenet.foo>', :size => 24).include?('size=24')
1281 assert avatar('jsmith <jsmith@somenet.foo>', :size => 24).include?('size=24')
1282 # Non-avatar options should be considered html options
1282 # Non-avatar options should be considered html options
1283 assert avatar('jsmith <jsmith@somenet.foo>', :title => 'John Smith').include?('title="John Smith"')
1283 assert avatar('jsmith <jsmith@somenet.foo>', :title => 'John Smith').include?('title="John Smith"')
1284 # The default class of the img tag should be gravatar
1284 # The default class of the img tag should be gravatar
1285 assert avatar('jsmith <jsmith@somenet.foo>').include?('class="gravatar"')
1285 assert avatar('jsmith <jsmith@somenet.foo>').include?('class="gravatar"')
1286 assert !avatar('jsmith <jsmith@somenet.foo>', :class => 'picture').include?('class="gravatar"')
1286 assert !avatar('jsmith <jsmith@somenet.foo>', :class => 'picture').include?('class="gravatar"')
1287 assert_nil avatar('jsmith')
1287 assert_nil avatar('jsmith')
1288 assert_nil avatar(nil)
1288 assert_nil avatar(nil)
1289 end
1289 end
1290 end
1290 end
1291
1291
1292 def test_avatar_disabled
1292 def test_avatar_disabled
1293 with_settings :gravatar_enabled => '0' do
1293 with_settings :gravatar_enabled => '0' do
1294 assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
1294 assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
1295 end
1295 end
1296 end
1296 end
1297
1297
1298 def test_link_to_user
1298 def test_link_to_user
1299 user = User.find(2)
1299 user = User.find(2)
1300 result = link_to("John Smith", "/users/2", :class => "user active")
1300 result = link_to("John Smith", "/users/2", :class => "user active")
1301 assert_equal result, link_to_user(user)
1301 assert_equal result, link_to_user(user)
1302 end
1302 end
1303
1303
1304 def test_link_to_user_should_not_link_to_locked_user
1304 def test_link_to_user_should_not_link_to_locked_user
1305 with_current_user nil do
1305 with_current_user nil do
1306 user = User.find(5)
1306 user = User.find(5)
1307 assert user.locked?
1307 assert user.locked?
1308 assert_equal 'Dave2 Lopper2', link_to_user(user)
1308 assert_equal 'Dave2 Lopper2', link_to_user(user)
1309 end
1309 end
1310 end
1310 end
1311
1311
1312 def test_link_to_user_should_link_to_locked_user_if_current_user_is_admin
1312 def test_link_to_user_should_link_to_locked_user_if_current_user_is_admin
1313 with_current_user User.find(1) do
1313 with_current_user User.find(1) do
1314 user = User.find(5)
1314 user = User.find(5)
1315 assert user.locked?
1315 assert user.locked?
1316 result = link_to("Dave2 Lopper2", "/users/5", :class => "user locked")
1316 result = link_to("Dave2 Lopper2", "/users/5", :class => "user locked")
1317 assert_equal result, link_to_user(user)
1317 assert_equal result, link_to_user(user)
1318 end
1318 end
1319 end
1319 end
1320
1320
1321 def test_link_to_user_should_not_link_to_anonymous
1321 def test_link_to_user_should_not_link_to_anonymous
1322 user = User.anonymous
1322 user = User.anonymous
1323 assert user.anonymous?
1323 assert user.anonymous?
1324 t = link_to_user(user)
1324 t = link_to_user(user)
1325 assert_equal ::I18n.t(:label_user_anonymous), t
1325 assert_equal ::I18n.t(:label_user_anonymous), t
1326 end
1326 end
1327
1327
1328 def test_link_to_attachment
1328 def test_link_to_attachment
1329 a = Attachment.find(3)
1329 a = Attachment.find(3)
1330 assert_equal '<a href="/attachments/3/logo.gif">logo.gif</a>',
1330 assert_equal '<a href="/attachments/3/logo.gif">logo.gif</a>',
1331 link_to_attachment(a)
1331 link_to_attachment(a)
1332 assert_equal '<a href="/attachments/3/logo.gif">Text</a>',
1332 assert_equal '<a href="/attachments/3/logo.gif">Text</a>',
1333 link_to_attachment(a, :text => 'Text')
1333 link_to_attachment(a, :text => 'Text')
1334 result = link_to("logo.gif", "/attachments/3/logo.gif", :class => "foo")
1334 result = link_to("logo.gif", "/attachments/3/logo.gif", :class => "foo")
1335 assert_equal result,
1335 assert_equal result,
1336 link_to_attachment(a, :class => 'foo')
1336 link_to_attachment(a, :class => 'foo')
1337 assert_equal '<a href="/attachments/download/3/logo.gif">logo.gif</a>',
1337 assert_equal '<a href="/attachments/download/3/logo.gif">logo.gif</a>',
1338 link_to_attachment(a, :download => true)
1338 link_to_attachment(a, :download => true)
1339 assert_equal '<a href="http://test.host/attachments/3/logo.gif">logo.gif</a>',
1339 assert_equal '<a href="http://test.host/attachments/3/logo.gif">logo.gif</a>',
1340 link_to_attachment(a, :only_path => false)
1340 link_to_attachment(a, :only_path => false)
1341 end
1341 end
1342
1342
1343 def test_thumbnail_tag
1343 def test_thumbnail_tag
1344 a = Attachment.find(3)
1344 a = Attachment.find(3)
1345 assert_select_in thumbnail_tag(a),
1345 assert_select_in thumbnail_tag(a),
1346 'a[href=?][title=?] img[alt="3"][src=?]',
1346 'a[href=?][title=?] img[alt="3"][src=?]',
1347 "/attachments/3/logo.gif", "logo.gif", "/attachments/thumbnail/3"
1347 "/attachments/3/logo.gif", "logo.gif", "/attachments/thumbnail/3"
1348 end
1348 end
1349
1349
1350 def test_link_to_project
1350 def test_link_to_project
1351 project = Project.find(1)
1351 project = Project.find(1)
1352 assert_equal %(<a href="/projects/ecookbook">eCookbook</a>),
1352 assert_equal %(<a href="/projects/ecookbook">eCookbook</a>),
1353 link_to_project(project)
1353 link_to_project(project)
1354 assert_equal %(<a href="http://test.host/projects/ecookbook?jump=blah">eCookbook</a>),
1354 assert_equal %(<a href="http://test.host/projects/ecookbook?jump=blah">eCookbook</a>),
1355 link_to_project(project, {:only_path => false, :jump => 'blah'})
1355 link_to_project(project, {:only_path => false, :jump => 'blah'})
1356 end
1356 end
1357
1357
1358 def test_link_to_project_settings
1358 def test_link_to_project_settings
1359 project = Project.find(1)
1359 project = Project.find(1)
1360 assert_equal '<a href="/projects/ecookbook/settings">eCookbook</a>', link_to_project_settings(project)
1360 assert_equal '<a href="/projects/ecookbook/settings">eCookbook</a>', link_to_project_settings(project)
1361
1361
1362 project.status = Project::STATUS_CLOSED
1362 project.status = Project::STATUS_CLOSED
1363 assert_equal '<a href="/projects/ecookbook">eCookbook</a>', link_to_project_settings(project)
1363 assert_equal '<a href="/projects/ecookbook">eCookbook</a>', link_to_project_settings(project)
1364
1364
1365 project.status = Project::STATUS_ARCHIVED
1365 project.status = Project::STATUS_ARCHIVED
1366 assert_equal 'eCookbook', link_to_project_settings(project)
1366 assert_equal 'eCookbook', link_to_project_settings(project)
1367 end
1367 end
1368
1368
1369 def test_link_to_legacy_project_with_numerical_identifier_should_use_id
1369 def test_link_to_legacy_project_with_numerical_identifier_should_use_id
1370 # numeric identifier are no longer allowed
1370 # numeric identifier are no longer allowed
1371 Project.where(:id => 1).update_all(:identifier => 25)
1371 Project.where(:id => 1).update_all(:identifier => 25)
1372 assert_equal '<a href="/projects/1">eCookbook</a>',
1372 assert_equal '<a href="/projects/1">eCookbook</a>',
1373 link_to_project(Project.find(1))
1373 link_to_project(Project.find(1))
1374 end
1374 end
1375
1375
1376 def test_principals_options_for_select_with_users
1376 def test_principals_options_for_select_with_users
1377 User.current = nil
1377 User.current = nil
1378 users = [User.find(2), User.find(4)]
1378 users = [User.find(2), User.find(4)]
1379 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>),
1379 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>),
1380 principals_options_for_select(users)
1380 principals_options_for_select(users)
1381 end
1381 end
1382
1382
1383 def test_principals_options_for_select_with_selected
1383 def test_principals_options_for_select_with_selected
1384 User.current = nil
1384 User.current = nil
1385 users = [User.find(2), User.find(4)]
1385 users = [User.find(2), User.find(4)]
1386 assert_equal %(<option value="2">John Smith</option><option value="4" selected="selected">Robert Hill</option>),
1386 assert_equal %(<option value="2">John Smith</option><option value="4" selected="selected">Robert Hill</option>),
1387 principals_options_for_select(users, User.find(4))
1387 principals_options_for_select(users, User.find(4))
1388 end
1388 end
1389
1389
1390 def test_principals_options_for_select_with_users_and_groups
1390 def test_principals_options_for_select_with_users_and_groups
1391 User.current = nil
1391 User.current = nil
1392 set_language_if_valid 'en'
1392 set_language_if_valid 'en'
1393 users = [User.find(2), Group.find(11), User.find(4), Group.find(10)]
1393 users = [User.find(2), Group.find(11), User.find(4), Group.find(10)]
1394 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>) +
1394 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>) +
1395 %(<optgroup label="Groups"><option value="10">A Team</option><option value="11">B Team</option></optgroup>),
1395 %(<optgroup label="Groups"><option value="10">A Team</option><option value="11">B Team</option></optgroup>),
1396 principals_options_for_select(users)
1396 principals_options_for_select(users)
1397 end
1397 end
1398
1398
1399 def test_principals_options_for_select_with_empty_collection
1399 def test_principals_options_for_select_with_empty_collection
1400 assert_equal '', principals_options_for_select([])
1400 assert_equal '', principals_options_for_select([])
1401 end
1401 end
1402
1402
1403 def test_principals_options_for_select_should_include_me_option_when_current_user_is_in_collection
1403 def test_principals_options_for_select_should_include_me_option_when_current_user_is_in_collection
1404 set_language_if_valid 'en'
1404 set_language_if_valid 'en'
1405 users = [User.find(2), User.find(4)]
1405 users = [User.find(2), User.find(4)]
1406 User.current = User.find(4)
1406 User.current = User.find(4)
1407 assert_include '<option value="4">&lt;&lt; me &gt;&gt;</option>', principals_options_for_select(users)
1407 assert_include '<option value="4">&lt;&lt; me &gt;&gt;</option>', principals_options_for_select(users)
1408 end
1408 end
1409
1409
1410 def test_stylesheet_link_tag_should_pick_the_default_stylesheet
1410 def test_stylesheet_link_tag_should_pick_the_default_stylesheet
1411 assert_match 'href="/stylesheets/styles.css"', stylesheet_link_tag("styles")
1411 assert_match 'href="/stylesheets/styles.css"', stylesheet_link_tag("styles")
1412 end
1412 end
1413
1413
1414 def test_stylesheet_link_tag_for_plugin_should_pick_the_plugin_stylesheet
1414 def test_stylesheet_link_tag_for_plugin_should_pick_the_plugin_stylesheet
1415 assert_match 'href="/plugin_assets/foo/stylesheets/styles.css"', stylesheet_link_tag("styles", :plugin => :foo)
1415 assert_match 'href="/plugin_assets/foo/stylesheets/styles.css"', stylesheet_link_tag("styles", :plugin => :foo)
1416 end
1416 end
1417
1417
1418 def test_image_tag_should_pick_the_default_image
1418 def test_image_tag_should_pick_the_default_image
1419 assert_match 'src="/images/image.png"', image_tag("image.png")
1419 assert_match 'src="/images/image.png"', image_tag("image.png")
1420 end
1420 end
1421
1421
1422 def test_image_tag_should_pick_the_theme_image_if_it_exists
1422 def test_image_tag_should_pick_the_theme_image_if_it_exists
1423 theme = Redmine::Themes.themes.last
1423 theme = Redmine::Themes.themes.last
1424 theme.images << 'image.png'
1424 theme.images << 'image.png'
1425
1425
1426 with_settings :ui_theme => theme.id do
1426 with_settings :ui_theme => theme.id do
1427 assert_match %|src="/themes/#{theme.dir}/images/image.png"|, image_tag("image.png")
1427 assert_match %|src="/themes/#{theme.dir}/images/image.png"|, image_tag("image.png")
1428 assert_match %|src="/images/other.png"|, image_tag("other.png")
1428 assert_match %|src="/images/other.png"|, image_tag("other.png")
1429 end
1429 end
1430 ensure
1430 ensure
1431 theme.images.delete 'image.png'
1431 theme.images.delete 'image.png'
1432 end
1432 end
1433
1433
1434 def test_image_tag_sfor_plugin_should_pick_the_plugin_image
1434 def test_image_tag_sfor_plugin_should_pick_the_plugin_image
1435 assert_match 'src="/plugin_assets/foo/images/image.png"', image_tag("image.png", :plugin => :foo)
1435 assert_match 'src="/plugin_assets/foo/images/image.png"', image_tag("image.png", :plugin => :foo)
1436 end
1436 end
1437
1437
1438 def test_javascript_include_tag_should_pick_the_default_javascript
1438 def test_javascript_include_tag_should_pick_the_default_javascript
1439 assert_match 'src="/javascripts/scripts.js"', javascript_include_tag("scripts")
1439 assert_match 'src="/javascripts/scripts.js"', javascript_include_tag("scripts")
1440 end
1440 end
1441
1441
1442 def test_javascript_include_tag_for_plugin_should_pick_the_plugin_javascript
1442 def test_javascript_include_tag_for_plugin_should_pick_the_plugin_javascript
1443 assert_match 'src="/plugin_assets/foo/javascripts/scripts.js"', javascript_include_tag("scripts", :plugin => :foo)
1443 assert_match 'src="/plugin_assets/foo/javascripts/scripts.js"', javascript_include_tag("scripts", :plugin => :foo)
1444 end
1444 end
1445
1445
1446 def test_raw_json_should_escape_closing_tags
1446 def test_raw_json_should_escape_closing_tags
1447 s = raw_json(["<foo>bar</foo>"])
1447 s = raw_json(["<foo>bar</foo>"])
1448 assert_include '\/foo', s
1448 assert_include '\/foo', s
1449 end
1449 end
1450
1450
1451 def test_raw_json_should_be_html_safe
1451 def test_raw_json_should_be_html_safe
1452 s = raw_json(["foo"])
1452 s = raw_json(["foo"])
1453 assert s.html_safe?
1453 assert s.html_safe?
1454 end
1454 end
1455
1455
1456 def test_html_title_should_app_title_if_not_set
1456 def test_html_title_should_app_title_if_not_set
1457 assert_equal 'Redmine', html_title
1457 assert_equal 'Redmine', html_title
1458 end
1458 end
1459
1459
1460 def test_html_title_should_join_items
1460 def test_html_title_should_join_items
1461 html_title 'Foo', 'Bar'
1461 html_title 'Foo', 'Bar'
1462 assert_equal 'Foo - Bar - Redmine', html_title
1462 assert_equal 'Foo - Bar - Redmine', html_title
1463 end
1463 end
1464
1464
1465 def test_html_title_should_append_current_project_name
1465 def test_html_title_should_append_current_project_name
1466 @project = Project.find(1)
1466 @project = Project.find(1)
1467 html_title 'Foo', 'Bar'
1467 html_title 'Foo', 'Bar'
1468 assert_equal 'Foo - Bar - eCookbook - Redmine', html_title
1468 assert_equal 'Foo - Bar - eCookbook - Redmine', html_title
1469 end
1469 end
1470
1470
1471 def test_title_should_return_a_h2_tag
1471 def test_title_should_return_a_h2_tag
1472 assert_equal '<h2>Foo</h2>', title('Foo')
1472 assert_equal '<h2>Foo</h2>', title('Foo')
1473 end
1473 end
1474
1474
1475 def test_title_should_set_html_title
1475 def test_title_should_set_html_title
1476 title('Foo')
1476 title('Foo')
1477 assert_equal 'Foo - Redmine', html_title
1477 assert_equal 'Foo - Redmine', html_title
1478 end
1478 end
1479
1479
1480 def test_title_should_turn_arrays_into_links
1480 def test_title_should_turn_arrays_into_links
1481 assert_equal '<h2><a href="/foo">Foo</a></h2>', title(['Foo', '/foo'])
1481 assert_equal '<h2><a href="/foo">Foo</a></h2>', title(['Foo', '/foo'])
1482 assert_equal 'Foo - Redmine', html_title
1482 assert_equal 'Foo - Redmine', html_title
1483 end
1483 end
1484
1484
1485 def test_title_should_join_items
1485 def test_title_should_join_items
1486 assert_equal '<h2>Foo &#187; Bar</h2>', title('Foo', 'Bar')
1486 assert_equal '<h2>Foo &#187; Bar</h2>', title('Foo', 'Bar')
1487 assert_equal 'Bar - Foo - Redmine', html_title
1487 assert_equal 'Bar - Foo - Redmine', html_title
1488 end
1488 end
1489
1489
1490 def test_favicon_path
1490 def test_favicon_path
1491 assert_match %r{^/favicon\.ico}, favicon_path
1491 assert_match %r{^/favicon\.ico}, favicon_path
1492 end
1492 end
1493
1493
1494 def test_favicon_path_with_suburi
1494 def test_favicon_path_with_suburi
1495 Redmine::Utils.relative_url_root = '/foo'
1495 Redmine::Utils.relative_url_root = '/foo'
1496 assert_match %r{^/foo/favicon\.ico}, favicon_path
1496 assert_match %r{^/foo/favicon\.ico}, favicon_path
1497 ensure
1497 ensure
1498 Redmine::Utils.relative_url_root = ''
1498 Redmine::Utils.relative_url_root = ''
1499 end
1499 end
1500
1500
1501 def test_favicon_url
1501 def test_favicon_url
1502 assert_match %r{^http://test\.host/favicon\.ico}, favicon_url
1502 assert_match %r{^http://test\.host/favicon\.ico}, favicon_url
1503 end
1503 end
1504
1504
1505 def test_favicon_url_with_suburi
1505 def test_favicon_url_with_suburi
1506 Redmine::Utils.relative_url_root = '/foo'
1506 Redmine::Utils.relative_url_root = '/foo'
1507 assert_match %r{^http://test\.host/foo/favicon\.ico}, favicon_url
1507 assert_match %r{^http://test\.host/foo/favicon\.ico}, favicon_url
1508 ensure
1508 ensure
1509 Redmine::Utils.relative_url_root = ''
1509 Redmine::Utils.relative_url_root = ''
1510 end
1510 end
1511
1511
1512 def test_truncate_single_line
1512 def test_truncate_single_line
1513 str = "01234"
1513 str = "01234"
1514 result = truncate_single_line_raw("#{str}\n#{str}", 10)
1514 result = truncate_single_line_raw("#{str}\n#{str}", 10)
1515 assert_equal "01234 0...", result
1515 assert_equal "01234 0...", result
1516 assert !result.html_safe?
1516 assert !result.html_safe?
1517 result = truncate_single_line_raw("#{str}<&#>\n#{str}#{str}", 16)
1517 result = truncate_single_line_raw("#{str}<&#>\n#{str}#{str}", 16)
1518 assert_equal "01234<&#> 012...", result
1518 assert_equal "01234<&#> 012...", result
1519 assert !result.html_safe?
1519 assert !result.html_safe?
1520 end
1520 end
1521
1521
1522 def test_truncate_single_line_non_ascii
1522 def test_truncate_single_line_non_ascii
1523 ja = "\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e".force_encoding('UTF-8')
1523 ja = "\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e".force_encoding('UTF-8')
1524 result = truncate_single_line_raw("#{ja}\n#{ja}\n#{ja}", 10)
1524 result = truncate_single_line_raw("#{ja}\n#{ja}\n#{ja}", 10)
1525 assert_equal "#{ja} #{ja}...", result
1525 assert_equal "#{ja} #{ja}...", result
1526 assert !result.html_safe?
1526 assert !result.html_safe?
1527 end
1527 end
1528 end
1528 end
@@ -1,853 +1,853
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class MailerTest < ActiveSupport::TestCase
20 class MailerTest < ActiveSupport::TestCase
21 include Redmine::I18n
21 include Redmine::I18n
22 include Rails::Dom::Testing::Assertions
22 include Rails::Dom::Testing::Assertions
23 fixtures :projects, :enabled_modules, :issues, :users, :email_addresses, :members,
23 fixtures :projects, :enabled_modules, :issues, :users, :email_addresses, :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 Setting.default_language = 'en'
37 Setting.default_language = 'en'
38 User.current = nil
38 User.current = nil
39 end
39 end
40
40
41 def test_generated_links_in_emails
41 def test_generated_links_in_emails
42 Setting.host_name = 'mydomain.foo'
42 Setting.host_name = 'mydomain.foo'
43 Setting.protocol = 'https'
43 Setting.protocol = 'https'
44
44
45 journal = Journal.find(3)
45 journal = Journal.find(3)
46 assert Mailer.deliver_issue_edit(journal)
46 assert Mailer.deliver_issue_edit(journal)
47
47
48 mail = last_email
48 mail = last_email
49 assert_not_nil mail
49 assert_not_nil mail
50
50
51 assert_select_email do
51 assert_select_email do
52 # link to the main ticket
52 # link to the main ticket
53 assert_select 'a[href=?]',
53 assert_select 'a[href=?]',
54 'https://mydomain.foo/issues/2#change-3',
54 'https://mydomain.foo/issues/2#change-3',
55 :text => 'Feature request #2: Add ingredients categories'
55 :text => 'Feature request #2: Add ingredients categories'
56 # link to a referenced ticket
56 # link to a referenced ticket
57 assert_select 'a[href=?][title=?]',
57 assert_select 'a[href=?][title=?]',
58 'https://mydomain.foo/issues/1',
58 'https://mydomain.foo/issues/1',
59 "Cannot print recipes (New)",
59 "Bug: Cannot print recipes (New)",
60 :text => '#1'
60 :text => '#1'
61 # link to a changeset
61 # link to a changeset
62 assert_select 'a[href=?][title=?]',
62 assert_select 'a[href=?][title=?]',
63 'https://mydomain.foo/projects/ecookbook/repository/revisions/2',
63 'https://mydomain.foo/projects/ecookbook/repository/revisions/2',
64 'This commit fixes #1, #2 and references #1 & #3',
64 'This commit fixes #1, #2 and references #1 & #3',
65 :text => 'r2'
65 :text => 'r2'
66 # link to a description diff
66 # link to a description diff
67 assert_select 'a[href^=?][title=?]',
67 assert_select 'a[href^=?][title=?]',
68 # should be https://mydomain.foo/journals/diff/3?detail_id=4
68 # should be https://mydomain.foo/journals/diff/3?detail_id=4
69 # but the Rails 4.2 DOM assertion doesn't handle the ? in the
69 # but the Rails 4.2 DOM assertion doesn't handle the ? in the
70 # attribute value
70 # attribute value
71 'https://mydomain.foo/journals/diff/3',
71 'https://mydomain.foo/journals/diff/3',
72 'View differences',
72 'View differences',
73 :text => 'diff'
73 :text => 'diff'
74 # link to an attachment
74 # link to an attachment
75 assert_select 'a[href=?]',
75 assert_select 'a[href=?]',
76 'https://mydomain.foo/attachments/download/4/source.rb',
76 'https://mydomain.foo/attachments/download/4/source.rb',
77 :text => 'source.rb'
77 :text => 'source.rb'
78 end
78 end
79 end
79 end
80
80
81 def test_generated_links_with_prefix
81 def test_generated_links_with_prefix
82 relative_url_root = Redmine::Utils.relative_url_root
82 relative_url_root = Redmine::Utils.relative_url_root
83 Setting.host_name = 'mydomain.foo/rdm'
83 Setting.host_name = 'mydomain.foo/rdm'
84 Setting.protocol = 'http'
84 Setting.protocol = 'http'
85
85
86 journal = Journal.find(3)
86 journal = Journal.find(3)
87 assert Mailer.deliver_issue_edit(journal)
87 assert Mailer.deliver_issue_edit(journal)
88
88
89 mail = last_email
89 mail = last_email
90 assert_not_nil mail
90 assert_not_nil mail
91
91
92 assert_select_email do
92 assert_select_email do
93 # link to the main ticket
93 # link to the main ticket
94 assert_select 'a[href=?]',
94 assert_select 'a[href=?]',
95 'http://mydomain.foo/rdm/issues/2#change-3',
95 'http://mydomain.foo/rdm/issues/2#change-3',
96 :text => 'Feature request #2: Add ingredients categories'
96 :text => 'Feature request #2: Add ingredients categories'
97 # link to a referenced ticket
97 # link to a referenced ticket
98 assert_select 'a[href=?][title=?]',
98 assert_select 'a[href=?][title=?]',
99 'http://mydomain.foo/rdm/issues/1',
99 'http://mydomain.foo/rdm/issues/1',
100 "Cannot print recipes (New)",
100 "Bug: Cannot print recipes (New)",
101 :text => '#1'
101 :text => '#1'
102 # link to a changeset
102 # link to a changeset
103 assert_select 'a[href=?][title=?]',
103 assert_select 'a[href=?][title=?]',
104 'http://mydomain.foo/rdm/projects/ecookbook/repository/revisions/2',
104 'http://mydomain.foo/rdm/projects/ecookbook/repository/revisions/2',
105 'This commit fixes #1, #2 and references #1 & #3',
105 'This commit fixes #1, #2 and references #1 & #3',
106 :text => 'r2'
106 :text => 'r2'
107 # link to a description diff
107 # link to a description diff
108 assert_select 'a[href^=?][title=?]',
108 assert_select 'a[href^=?][title=?]',
109 # should be http://mydomain.foo/rdm/journals/diff/3?detail_id=4
109 # should be http://mydomain.foo/rdm/journals/diff/3?detail_id=4
110 # but the Rails 4.2 DOM assertion doesn't handle the ? in the
110 # but the Rails 4.2 DOM assertion doesn't handle the ? in the
111 # attribute value
111 # attribute value
112 'http://mydomain.foo/rdm/journals/diff/3',
112 'http://mydomain.foo/rdm/journals/diff/3',
113 'View differences',
113 'View differences',
114 :text => 'diff'
114 :text => 'diff'
115 # link to an attachment
115 # link to an attachment
116 assert_select 'a[href=?]',
116 assert_select 'a[href=?]',
117 'http://mydomain.foo/rdm/attachments/download/4/source.rb',
117 'http://mydomain.foo/rdm/attachments/download/4/source.rb',
118 :text => 'source.rb'
118 :text => 'source.rb'
119 end
119 end
120 end
120 end
121
121
122 def test_generated_links_with_port_and_prefix
122 def test_generated_links_with_port_and_prefix
123 with_settings :host_name => '10.0.0.1:81/redmine', :protocol => 'http' do
123 with_settings :host_name => '10.0.0.1:81/redmine', :protocol => 'http' do
124 Mailer.test_email(User.find(1)).deliver
124 Mailer.test_email(User.find(1)).deliver
125 mail = last_email
125 mail = last_email
126 assert_not_nil mail
126 assert_not_nil mail
127 assert_include 'http://10.0.0.1:81/redmine', mail_body(mail)
127 assert_include 'http://10.0.0.1:81/redmine', mail_body(mail)
128 end
128 end
129 end
129 end
130
130
131 def test_generated_links_with_port
131 def test_generated_links_with_port
132 with_settings :host_name => '10.0.0.1:81', :protocol => 'http' do
132 with_settings :host_name => '10.0.0.1:81', :protocol => 'http' do
133 Mailer.test_email(User.find(1)).deliver
133 Mailer.test_email(User.find(1)).deliver
134 mail = last_email
134 mail = last_email
135 assert_not_nil mail
135 assert_not_nil mail
136 assert_include 'http://10.0.0.1:81', mail_body(mail)
136 assert_include 'http://10.0.0.1:81', mail_body(mail)
137 end
137 end
138 end
138 end
139
139
140 def test_issue_edit_should_generate_url_with_hostname_for_relations
140 def test_issue_edit_should_generate_url_with_hostname_for_relations
141 journal = Journal.new(:journalized => Issue.find(1), :user => User.find(1), :created_on => Time.now)
141 journal = Journal.new(:journalized => Issue.find(1), :user => User.find(1), :created_on => Time.now)
142 journal.details << JournalDetail.new(:property => 'relation', :prop_key => 'label_relates_to', :value => 2)
142 journal.details << JournalDetail.new(:property => 'relation', :prop_key => 'label_relates_to', :value => 2)
143 Mailer.deliver_issue_edit(journal)
143 Mailer.deliver_issue_edit(journal)
144 assert_not_nil last_email
144 assert_not_nil last_email
145 assert_select_email do
145 assert_select_email do
146 assert_select 'a[href=?]', 'http://mydomain.foo/issues/2', :text => 'Feature request #2'
146 assert_select 'a[href=?]', 'http://mydomain.foo/issues/2', :text => 'Feature request #2'
147 end
147 end
148 end
148 end
149
149
150 def test_generated_links_with_prefix_and_no_relative_url_root
150 def test_generated_links_with_prefix_and_no_relative_url_root
151 relative_url_root = Redmine::Utils.relative_url_root
151 relative_url_root = Redmine::Utils.relative_url_root
152 Setting.host_name = 'mydomain.foo/rdm'
152 Setting.host_name = 'mydomain.foo/rdm'
153 Setting.protocol = 'http'
153 Setting.protocol = 'http'
154 Redmine::Utils.relative_url_root = nil
154 Redmine::Utils.relative_url_root = nil
155
155
156 journal = Journal.find(3)
156 journal = Journal.find(3)
157 assert Mailer.deliver_issue_edit(journal)
157 assert Mailer.deliver_issue_edit(journal)
158
158
159 mail = last_email
159 mail = last_email
160 assert_not_nil mail
160 assert_not_nil mail
161
161
162 assert_select_email do
162 assert_select_email do
163 # link to the main ticket
163 # link to the main ticket
164 assert_select 'a[href=?]',
164 assert_select 'a[href=?]',
165 'http://mydomain.foo/rdm/issues/2#change-3',
165 'http://mydomain.foo/rdm/issues/2#change-3',
166 :text => 'Feature request #2: Add ingredients categories'
166 :text => 'Feature request #2: Add ingredients categories'
167 # link to a referenced ticket
167 # link to a referenced ticket
168 assert_select 'a[href=?][title=?]',
168 assert_select 'a[href=?][title=?]',
169 'http://mydomain.foo/rdm/issues/1',
169 'http://mydomain.foo/rdm/issues/1',
170 "Cannot print recipes (New)",
170 "Bug: Cannot print recipes (New)",
171 :text => '#1'
171 :text => '#1'
172 # link to a changeset
172 # link to a changeset
173 assert_select 'a[href=?][title=?]',
173 assert_select 'a[href=?][title=?]',
174 'http://mydomain.foo/rdm/projects/ecookbook/repository/revisions/2',
174 'http://mydomain.foo/rdm/projects/ecookbook/repository/revisions/2',
175 'This commit fixes #1, #2 and references #1 & #3',
175 'This commit fixes #1, #2 and references #1 & #3',
176 :text => 'r2'
176 :text => 'r2'
177 # link to a description diff
177 # link to a description diff
178 assert_select 'a[href^=?][title=?]',
178 assert_select 'a[href^=?][title=?]',
179 # should be http://mydomain.foo/rdm/journals/diff/3?detail_id=4
179 # should be http://mydomain.foo/rdm/journals/diff/3?detail_id=4
180 # but the Rails 4.2 DOM assertion doesn't handle the ? in the
180 # but the Rails 4.2 DOM assertion doesn't handle the ? in the
181 # attribute value
181 # attribute value
182 'http://mydomain.foo/rdm/journals/diff/3',
182 'http://mydomain.foo/rdm/journals/diff/3',
183 'View differences',
183 'View differences',
184 :text => 'diff'
184 :text => 'diff'
185 # link to an attachment
185 # link to an attachment
186 assert_select 'a[href=?]',
186 assert_select 'a[href=?]',
187 'http://mydomain.foo/rdm/attachments/download/4/source.rb',
187 'http://mydomain.foo/rdm/attachments/download/4/source.rb',
188 :text => 'source.rb'
188 :text => 'source.rb'
189 end
189 end
190 ensure
190 ensure
191 # restore it
191 # restore it
192 Redmine::Utils.relative_url_root = relative_url_root
192 Redmine::Utils.relative_url_root = relative_url_root
193 end
193 end
194
194
195 def test_email_headers
195 def test_email_headers
196 issue = Issue.find(1)
196 issue = Issue.find(1)
197 Mailer.deliver_issue_add(issue)
197 Mailer.deliver_issue_add(issue)
198 mail = last_email
198 mail = last_email
199 assert_not_nil mail
199 assert_not_nil mail
200 assert_equal 'All', mail.header['X-Auto-Response-Suppress'].to_s
200 assert_equal 'All', mail.header['X-Auto-Response-Suppress'].to_s
201 assert_equal 'auto-generated', mail.header['Auto-Submitted'].to_s
201 assert_equal 'auto-generated', mail.header['Auto-Submitted'].to_s
202 assert_equal '<redmine.example.net>', mail.header['List-Id'].to_s
202 assert_equal '<redmine.example.net>', mail.header['List-Id'].to_s
203 end
203 end
204
204
205 def test_email_headers_should_include_sender
205 def test_email_headers_should_include_sender
206 issue = Issue.find(1)
206 issue = Issue.find(1)
207 Mailer.deliver_issue_add(issue)
207 Mailer.deliver_issue_add(issue)
208 mail = last_email
208 mail = last_email
209 assert_equal issue.author.login, mail.header['X-Redmine-Sender'].to_s
209 assert_equal issue.author.login, mail.header['X-Redmine-Sender'].to_s
210 end
210 end
211
211
212 def test_plain_text_mail
212 def test_plain_text_mail
213 Setting.plain_text_mail = 1
213 Setting.plain_text_mail = 1
214 journal = Journal.find(2)
214 journal = Journal.find(2)
215 Mailer.deliver_issue_edit(journal)
215 Mailer.deliver_issue_edit(journal)
216 mail = last_email
216 mail = last_email
217 assert_equal "text/plain; charset=UTF-8", mail.content_type
217 assert_equal "text/plain; charset=UTF-8", mail.content_type
218 assert_equal 0, mail.parts.size
218 assert_equal 0, mail.parts.size
219 assert !mail.encoded.include?('href')
219 assert !mail.encoded.include?('href')
220 end
220 end
221
221
222 def test_html_mail
222 def test_html_mail
223 Setting.plain_text_mail = 0
223 Setting.plain_text_mail = 0
224 journal = Journal.find(2)
224 journal = Journal.find(2)
225 Mailer.deliver_issue_edit(journal)
225 Mailer.deliver_issue_edit(journal)
226 mail = last_email
226 mail = last_email
227 assert_equal 2, mail.parts.size
227 assert_equal 2, mail.parts.size
228 assert mail.encoded.include?('href')
228 assert mail.encoded.include?('href')
229 end
229 end
230
230
231 def test_from_header
231 def test_from_header
232 with_settings :mail_from => 'redmine@example.net' do
232 with_settings :mail_from => 'redmine@example.net' do
233 Mailer.test_email(User.find(1)).deliver
233 Mailer.test_email(User.find(1)).deliver
234 end
234 end
235 mail = last_email
235 mail = last_email
236 assert_equal 'redmine@example.net', mail.from_addrs.first
236 assert_equal 'redmine@example.net', mail.from_addrs.first
237 end
237 end
238
238
239 def test_from_header_with_phrase
239 def test_from_header_with_phrase
240 with_settings :mail_from => 'Redmine app <redmine@example.net>' do
240 with_settings :mail_from => 'Redmine app <redmine@example.net>' do
241 Mailer.test_email(User.find(1)).deliver
241 Mailer.test_email(User.find(1)).deliver
242 end
242 end
243 mail = last_email
243 mail = last_email
244 assert_equal 'redmine@example.net', mail.from_addrs.first
244 assert_equal 'redmine@example.net', mail.from_addrs.first
245 assert_equal 'Redmine app <redmine@example.net>', mail.header['From'].to_s
245 assert_equal 'Redmine app <redmine@example.net>', mail.header['From'].to_s
246 end
246 end
247
247
248 def test_should_not_send_email_without_recipient
248 def test_should_not_send_email_without_recipient
249 news = News.first
249 news = News.first
250 user = news.author
250 user = news.author
251 # Remove members except news author
251 # Remove members except news author
252 news.project.memberships.each {|m| m.destroy unless m.user == user}
252 news.project.memberships.each {|m| m.destroy unless m.user == user}
253
253
254 user.pref.no_self_notified = false
254 user.pref.no_self_notified = false
255 user.pref.save
255 user.pref.save
256 User.current = user
256 User.current = user
257 Mailer.news_added(news.reload).deliver
257 Mailer.news_added(news.reload).deliver
258 assert_equal 1, last_email.bcc.size
258 assert_equal 1, last_email.bcc.size
259
259
260 # nobody to notify
260 # nobody to notify
261 user.pref.no_self_notified = true
261 user.pref.no_self_notified = true
262 user.pref.save
262 user.pref.save
263 User.current = user
263 User.current = user
264 ActionMailer::Base.deliveries.clear
264 ActionMailer::Base.deliveries.clear
265 Mailer.news_added(news.reload).deliver
265 Mailer.news_added(news.reload).deliver
266 assert ActionMailer::Base.deliveries.empty?
266 assert ActionMailer::Base.deliveries.empty?
267 end
267 end
268
268
269 def test_issue_add_message_id
269 def test_issue_add_message_id
270 issue = Issue.find(2)
270 issue = Issue.find(2)
271 Mailer.deliver_issue_add(issue)
271 Mailer.deliver_issue_add(issue)
272 mail = last_email
272 mail = last_email
273 assert_match /^redmine\.issue-2\.20060719190421\.[a-f0-9]+@example\.net/, mail.message_id
273 assert_match /^redmine\.issue-2\.20060719190421\.[a-f0-9]+@example\.net/, mail.message_id
274 assert_include "redmine.issue-2.20060719190421@example.net", mail.references
274 assert_include "redmine.issue-2.20060719190421@example.net", mail.references
275 end
275 end
276
276
277 def test_issue_edit_message_id
277 def test_issue_edit_message_id
278 journal = Journal.find(3)
278 journal = Journal.find(3)
279 journal.issue = Issue.find(2)
279 journal.issue = Issue.find(2)
280
280
281 Mailer.deliver_issue_edit(journal)
281 Mailer.deliver_issue_edit(journal)
282 mail = last_email
282 mail = last_email
283 assert_match /^redmine\.journal-3\.\d+\.[a-f0-9]+@example\.net/, mail.message_id
283 assert_match /^redmine\.journal-3\.\d+\.[a-f0-9]+@example\.net/, mail.message_id
284 assert_include "redmine.issue-2.20060719190421@example.net", mail.references
284 assert_include "redmine.issue-2.20060719190421@example.net", mail.references
285 assert_select_email do
285 assert_select_email do
286 # link to the update
286 # link to the update
287 assert_select "a[href=?]",
287 assert_select "a[href=?]",
288 "http://mydomain.foo/issues/#{journal.journalized_id}#change-#{journal.id}"
288 "http://mydomain.foo/issues/#{journal.journalized_id}#change-#{journal.id}"
289 end
289 end
290 end
290 end
291
291
292 def test_message_posted_message_id
292 def test_message_posted_message_id
293 message = Message.find(1)
293 message = Message.find(1)
294 Mailer.message_posted(message).deliver
294 Mailer.message_posted(message).deliver
295 mail = last_email
295 mail = last_email
296 assert_match /^redmine\.message-1\.\d+\.[a-f0-9]+@example\.net/, mail.message_id
296 assert_match /^redmine\.message-1\.\d+\.[a-f0-9]+@example\.net/, mail.message_id
297 assert_include "redmine.message-1.20070512151532@example.net", mail.references
297 assert_include "redmine.message-1.20070512151532@example.net", mail.references
298 assert_select_email do
298 assert_select_email do
299 # link to the message
299 # link to the message
300 assert_select "a[href=?]",
300 assert_select "a[href=?]",
301 "http://mydomain.foo/boards/#{message.board.id}/topics/#{message.id}",
301 "http://mydomain.foo/boards/#{message.board.id}/topics/#{message.id}",
302 :text => message.subject
302 :text => message.subject
303 end
303 end
304 end
304 end
305
305
306 def test_reply_posted_message_id
306 def test_reply_posted_message_id
307 message = Message.find(3)
307 message = Message.find(3)
308 Mailer.message_posted(message).deliver
308 Mailer.message_posted(message).deliver
309 mail = last_email
309 mail = last_email
310 assert_match /^redmine\.message-3\.\d+\.[a-f0-9]+@example\.net/, mail.message_id
310 assert_match /^redmine\.message-3\.\d+\.[a-f0-9]+@example\.net/, mail.message_id
311 assert_include "redmine.message-1.20070512151532@example.net", mail.references
311 assert_include "redmine.message-1.20070512151532@example.net", mail.references
312 assert_select_email do
312 assert_select_email do
313 # link to the reply
313 # link to the reply
314 assert_select "a[href=?]",
314 assert_select "a[href=?]",
315 "http://mydomain.foo/boards/#{message.board.id}/topics/#{message.root.id}?r=#{message.id}#message-#{message.id}",
315 "http://mydomain.foo/boards/#{message.board.id}/topics/#{message.root.id}?r=#{message.id}#message-#{message.id}",
316 :text => message.subject
316 :text => message.subject
317 end
317 end
318 end
318 end
319
319
320 test "#issue_add should notify project members" do
320 test "#issue_add should notify project members" do
321 issue = Issue.find(1)
321 issue = Issue.find(1)
322 assert Mailer.deliver_issue_add(issue)
322 assert Mailer.deliver_issue_add(issue)
323 assert last_email.bcc.include?('dlopper@somenet.foo')
323 assert last_email.bcc.include?('dlopper@somenet.foo')
324 end
324 end
325
325
326 def test_issue_add_should_send_mail_to_all_user_email_address
326 def test_issue_add_should_send_mail_to_all_user_email_address
327 EmailAddress.create!(:user_id => 3, :address => 'otheremail@somenet.foo')
327 EmailAddress.create!(:user_id => 3, :address => 'otheremail@somenet.foo')
328 issue = Issue.find(1)
328 issue = Issue.find(1)
329 assert Mailer.deliver_issue_add(issue)
329 assert Mailer.deliver_issue_add(issue)
330 assert last_email.bcc.include?('dlopper@somenet.foo')
330 assert last_email.bcc.include?('dlopper@somenet.foo')
331 assert last_email.bcc.include?('otheremail@somenet.foo')
331 assert last_email.bcc.include?('otheremail@somenet.foo')
332 end
332 end
333
333
334 test "#issue_add should not notify project members that are not allow to view the issue" do
334 test "#issue_add should not notify project members that are not allow to view the issue" do
335 issue = Issue.find(1)
335 issue = Issue.find(1)
336 Role.find(2).remove_permission!(:view_issues)
336 Role.find(2).remove_permission!(:view_issues)
337 assert Mailer.deliver_issue_add(issue)
337 assert Mailer.deliver_issue_add(issue)
338 assert !last_email.bcc.include?('dlopper@somenet.foo')
338 assert !last_email.bcc.include?('dlopper@somenet.foo')
339 end
339 end
340
340
341 test "#issue_add should notify issue watchers" do
341 test "#issue_add should notify issue watchers" do
342 issue = Issue.find(1)
342 issue = Issue.find(1)
343 user = User.find(9)
343 user = User.find(9)
344 # minimal email notification options
344 # minimal email notification options
345 user.pref.no_self_notified = '1'
345 user.pref.no_self_notified = '1'
346 user.pref.save
346 user.pref.save
347 user.mail_notification = false
347 user.mail_notification = false
348 user.save
348 user.save
349
349
350 Watcher.create!(:watchable => issue, :user => user)
350 Watcher.create!(:watchable => issue, :user => user)
351 assert Mailer.deliver_issue_add(issue)
351 assert Mailer.deliver_issue_add(issue)
352 assert last_email.bcc.include?(user.mail)
352 assert last_email.bcc.include?(user.mail)
353 end
353 end
354
354
355 test "#issue_add should not notify watchers not allowed to view the issue" do
355 test "#issue_add should not notify watchers not allowed to view the issue" do
356 issue = Issue.find(1)
356 issue = Issue.find(1)
357 user = User.find(9)
357 user = User.find(9)
358 Watcher.create!(:watchable => issue, :user => user)
358 Watcher.create!(:watchable => issue, :user => user)
359 Role.non_member.remove_permission!(:view_issues)
359 Role.non_member.remove_permission!(:view_issues)
360 assert Mailer.deliver_issue_add(issue)
360 assert Mailer.deliver_issue_add(issue)
361 assert !last_email.bcc.include?(user.mail)
361 assert !last_email.bcc.include?(user.mail)
362 end
362 end
363
363
364 def test_issue_add_should_include_enabled_fields
364 def test_issue_add_should_include_enabled_fields
365 issue = Issue.find(2)
365 issue = Issue.find(2)
366 assert Mailer.deliver_issue_add(issue)
366 assert Mailer.deliver_issue_add(issue)
367 assert_mail_body_match '* Target version: 1.0', last_email
367 assert_mail_body_match '* Target version: 1.0', last_email
368 assert_select_email do
368 assert_select_email do
369 assert_select 'li', :text => 'Target version: 1.0'
369 assert_select 'li', :text => 'Target version: 1.0'
370 end
370 end
371 end
371 end
372
372
373 def test_issue_add_should_not_include_disabled_fields
373 def test_issue_add_should_not_include_disabled_fields
374 issue = Issue.find(2)
374 issue = Issue.find(2)
375 tracker = issue.tracker
375 tracker = issue.tracker
376 tracker.core_fields -= ['fixed_version_id']
376 tracker.core_fields -= ['fixed_version_id']
377 tracker.save!
377 tracker.save!
378 assert Mailer.deliver_issue_add(issue)
378 assert Mailer.deliver_issue_add(issue)
379 assert_mail_body_no_match 'Target version', last_email
379 assert_mail_body_no_match 'Target version', last_email
380 assert_select_email do
380 assert_select_email do
381 assert_select 'li', :text => /Target version/, :count => 0
381 assert_select 'li', :text => /Target version/, :count => 0
382 end
382 end
383 end
383 end
384
384
385 # test mailer methods for each language
385 # test mailer methods for each language
386 def test_issue_add
386 def test_issue_add
387 issue = Issue.find(1)
387 issue = Issue.find(1)
388 with_each_language_as_default do
388 with_each_language_as_default do
389 assert Mailer.deliver_issue_add(issue)
389 assert Mailer.deliver_issue_add(issue)
390 end
390 end
391 end
391 end
392
392
393 def test_issue_edit
393 def test_issue_edit
394 journal = Journal.find(1)
394 journal = Journal.find(1)
395 with_each_language_as_default do
395 with_each_language_as_default do
396 assert Mailer.deliver_issue_edit(journal)
396 assert Mailer.deliver_issue_edit(journal)
397 end
397 end
398 end
398 end
399
399
400 def test_issue_edit_should_send_private_notes_to_users_with_permission_only
400 def test_issue_edit_should_send_private_notes_to_users_with_permission_only
401 journal = Journal.find(1)
401 journal = Journal.find(1)
402 journal.private_notes = true
402 journal.private_notes = true
403 journal.save!
403 journal.save!
404
404
405 Role.find(2).add_permission! :view_private_notes
405 Role.find(2).add_permission! :view_private_notes
406 Mailer.deliver_issue_edit(journal)
406 Mailer.deliver_issue_edit(journal)
407 assert_equal %w(dlopper@somenet.foo jsmith@somenet.foo), ActionMailer::Base.deliveries.last.bcc.sort
407 assert_equal %w(dlopper@somenet.foo jsmith@somenet.foo), ActionMailer::Base.deliveries.last.bcc.sort
408
408
409 Role.find(2).remove_permission! :view_private_notes
409 Role.find(2).remove_permission! :view_private_notes
410 Mailer.deliver_issue_edit(journal)
410 Mailer.deliver_issue_edit(journal)
411 assert_equal %w(jsmith@somenet.foo), ActionMailer::Base.deliveries.last.bcc.sort
411 assert_equal %w(jsmith@somenet.foo), ActionMailer::Base.deliveries.last.bcc.sort
412 end
412 end
413
413
414 def test_issue_edit_should_send_private_notes_to_watchers_with_permission_only
414 def test_issue_edit_should_send_private_notes_to_watchers_with_permission_only
415 Issue.find(1).set_watcher(User.find_by_login('someone'))
415 Issue.find(1).set_watcher(User.find_by_login('someone'))
416 journal = Journal.find(1)
416 journal = Journal.find(1)
417 journal.private_notes = true
417 journal.private_notes = true
418 journal.save!
418 journal.save!
419
419
420 Role.non_member.add_permission! :view_private_notes
420 Role.non_member.add_permission! :view_private_notes
421 Mailer.deliver_issue_edit(journal)
421 Mailer.deliver_issue_edit(journal)
422 assert_include 'someone@foo.bar', ActionMailer::Base.deliveries.last.bcc.sort
422 assert_include 'someone@foo.bar', ActionMailer::Base.deliveries.last.bcc.sort
423
423
424 Role.non_member.remove_permission! :view_private_notes
424 Role.non_member.remove_permission! :view_private_notes
425 Mailer.deliver_issue_edit(journal)
425 Mailer.deliver_issue_edit(journal)
426 assert_not_include 'someone@foo.bar', ActionMailer::Base.deliveries.last.bcc.sort
426 assert_not_include 'someone@foo.bar', ActionMailer::Base.deliveries.last.bcc.sort
427 end
427 end
428
428
429 def test_issue_edit_should_mark_private_notes
429 def test_issue_edit_should_mark_private_notes
430 journal = Journal.find(2)
430 journal = Journal.find(2)
431 journal.private_notes = true
431 journal.private_notes = true
432 journal.save!
432 journal.save!
433
433
434 with_settings :default_language => 'en' do
434 with_settings :default_language => 'en' do
435 Mailer.deliver_issue_edit(journal)
435 Mailer.deliver_issue_edit(journal)
436 end
436 end
437 assert_mail_body_match '(Private notes)', last_email
437 assert_mail_body_match '(Private notes)', last_email
438 end
438 end
439
439
440 def test_issue_edit_with_relation_should_notify_users_who_can_see_the_related_issue
440 def test_issue_edit_with_relation_should_notify_users_who_can_see_the_related_issue
441 issue = Issue.generate!
441 issue = Issue.generate!
442 issue.init_journal(User.find(1))
442 issue.init_journal(User.find(1))
443 private_issue = Issue.generate!(:is_private => true)
443 private_issue = Issue.generate!(:is_private => true)
444 IssueRelation.create!(:issue_from => issue, :issue_to => private_issue, :relation_type => 'relates')
444 IssueRelation.create!(:issue_from => issue, :issue_to => private_issue, :relation_type => 'relates')
445 issue.reload
445 issue.reload
446 assert_equal 1, issue.journals.size
446 assert_equal 1, issue.journals.size
447 journal = issue.journals.first
447 journal = issue.journals.first
448 ActionMailer::Base.deliveries.clear
448 ActionMailer::Base.deliveries.clear
449
449
450 Mailer.deliver_issue_edit(journal)
450 Mailer.deliver_issue_edit(journal)
451 last_email.bcc.each do |email|
451 last_email.bcc.each do |email|
452 user = User.find_by_mail(email)
452 user = User.find_by_mail(email)
453 assert private_issue.visible?(user), "Issue was not visible to #{user}"
453 assert private_issue.visible?(user), "Issue was not visible to #{user}"
454 end
454 end
455 end
455 end
456
456
457 def test_document_added
457 def test_document_added
458 document = Document.find(1)
458 document = Document.find(1)
459 with_each_language_as_default do
459 with_each_language_as_default do
460 assert Mailer.document_added(document).deliver
460 assert Mailer.document_added(document).deliver
461 end
461 end
462 end
462 end
463
463
464 def test_attachments_added
464 def test_attachments_added
465 attachements = [ Attachment.find_by_container_type('Document') ]
465 attachements = [ Attachment.find_by_container_type('Document') ]
466 with_each_language_as_default do
466 with_each_language_as_default do
467 assert Mailer.attachments_added(attachements).deliver
467 assert Mailer.attachments_added(attachements).deliver
468 end
468 end
469 end
469 end
470
470
471 def test_version_file_added
471 def test_version_file_added
472 attachements = [ Attachment.find_by_container_type('Version') ]
472 attachements = [ Attachment.find_by_container_type('Version') ]
473 assert Mailer.attachments_added(attachements).deliver
473 assert Mailer.attachments_added(attachements).deliver
474 assert_not_nil last_email.bcc
474 assert_not_nil last_email.bcc
475 assert last_email.bcc.any?
475 assert last_email.bcc.any?
476 assert_select_email do
476 assert_select_email do
477 assert_select "a[href=?]", "http://mydomain.foo/projects/ecookbook/files"
477 assert_select "a[href=?]", "http://mydomain.foo/projects/ecookbook/files"
478 end
478 end
479 end
479 end
480
480
481 def test_project_file_added
481 def test_project_file_added
482 attachements = [ Attachment.find_by_container_type('Project') ]
482 attachements = [ Attachment.find_by_container_type('Project') ]
483 assert Mailer.attachments_added(attachements).deliver
483 assert Mailer.attachments_added(attachements).deliver
484 assert_not_nil last_email.bcc
484 assert_not_nil last_email.bcc
485 assert last_email.bcc.any?
485 assert last_email.bcc.any?
486 assert_select_email do
486 assert_select_email do
487 assert_select "a[href=?]", "http://mydomain.foo/projects/ecookbook/files"
487 assert_select "a[href=?]", "http://mydomain.foo/projects/ecookbook/files"
488 end
488 end
489 end
489 end
490
490
491 def test_news_added
491 def test_news_added
492 news = News.first
492 news = News.first
493 with_each_language_as_default do
493 with_each_language_as_default do
494 assert Mailer.news_added(news).deliver
494 assert Mailer.news_added(news).deliver
495 end
495 end
496 end
496 end
497
497
498 def test_news_added_should_notify_project_news_watchers
498 def test_news_added_should_notify_project_news_watchers
499 user1 = User.generate!
499 user1 = User.generate!
500 user2 = User.generate!
500 user2 = User.generate!
501 news = News.find(1)
501 news = News.find(1)
502 news.project.enabled_module('news').add_watcher(user1)
502 news.project.enabled_module('news').add_watcher(user1)
503
503
504 Mailer.news_added(news).deliver
504 Mailer.news_added(news).deliver
505 assert_include user1.mail, last_email.bcc
505 assert_include user1.mail, last_email.bcc
506 assert_not_include user2.mail, last_email.bcc
506 assert_not_include user2.mail, last_email.bcc
507 end
507 end
508
508
509 def test_news_comment_added
509 def test_news_comment_added
510 comment = Comment.find(2)
510 comment = Comment.find(2)
511 with_each_language_as_default do
511 with_each_language_as_default do
512 assert Mailer.news_comment_added(comment).deliver
512 assert Mailer.news_comment_added(comment).deliver
513 end
513 end
514 end
514 end
515
515
516 def test_message_posted
516 def test_message_posted
517 message = Message.first
517 message = Message.first
518 recipients = ([message.root] + message.root.children).collect {|m| m.author.mail if m.author}
518 recipients = ([message.root] + message.root.children).collect {|m| m.author.mail if m.author}
519 recipients = recipients.compact.uniq
519 recipients = recipients.compact.uniq
520 with_each_language_as_default do
520 with_each_language_as_default do
521 assert Mailer.message_posted(message).deliver
521 assert Mailer.message_posted(message).deliver
522 end
522 end
523 end
523 end
524
524
525 def test_wiki_content_added
525 def test_wiki_content_added
526 content = WikiContent.find(1)
526 content = WikiContent.find(1)
527 with_each_language_as_default do
527 with_each_language_as_default do
528 assert_difference 'ActionMailer::Base.deliveries.size' do
528 assert_difference 'ActionMailer::Base.deliveries.size' do
529 assert Mailer.wiki_content_added(content).deliver
529 assert Mailer.wiki_content_added(content).deliver
530 assert_select_email do
530 assert_select_email do
531 assert_select 'a[href=?]',
531 assert_select 'a[href=?]',
532 'http://mydomain.foo/projects/ecookbook/wiki/CookBook_documentation',
532 'http://mydomain.foo/projects/ecookbook/wiki/CookBook_documentation',
533 :text => 'CookBook documentation'
533 :text => 'CookBook documentation'
534 end
534 end
535 end
535 end
536 end
536 end
537 end
537 end
538
538
539 def test_wiki_content_updated
539 def test_wiki_content_updated
540 content = WikiContent.find(1)
540 content = WikiContent.find(1)
541 with_each_language_as_default do
541 with_each_language_as_default do
542 assert_difference 'ActionMailer::Base.deliveries.size' do
542 assert_difference 'ActionMailer::Base.deliveries.size' do
543 assert Mailer.wiki_content_updated(content).deliver
543 assert Mailer.wiki_content_updated(content).deliver
544 assert_select_email do
544 assert_select_email do
545 assert_select 'a[href=?]',
545 assert_select 'a[href=?]',
546 'http://mydomain.foo/projects/ecookbook/wiki/CookBook_documentation',
546 'http://mydomain.foo/projects/ecookbook/wiki/CookBook_documentation',
547 :text => 'CookBook documentation'
547 :text => 'CookBook documentation'
548 end
548 end
549 end
549 end
550 end
550 end
551 end
551 end
552
552
553 def test_account_information
553 def test_account_information
554 user = User.find(2)
554 user = User.find(2)
555 valid_languages.each do |lang|
555 valid_languages.each do |lang|
556 user.update_attribute :language, lang.to_s
556 user.update_attribute :language, lang.to_s
557 user.reload
557 user.reload
558 assert Mailer.account_information(user, 'pAsswORd').deliver
558 assert Mailer.account_information(user, 'pAsswORd').deliver
559 end
559 end
560 end
560 end
561
561
562 def test_lost_password
562 def test_lost_password
563 token = Token.find(2)
563 token = Token.find(2)
564 valid_languages.each do |lang|
564 valid_languages.each do |lang|
565 token.user.update_attribute :language, lang.to_s
565 token.user.update_attribute :language, lang.to_s
566 token.reload
566 token.reload
567 assert Mailer.lost_password(token).deliver
567 assert Mailer.lost_password(token).deliver
568 end
568 end
569 end
569 end
570
570
571 def test_register
571 def test_register
572 token = Token.find(1)
572 token = Token.find(1)
573 Setting.host_name = 'redmine.foo'
573 Setting.host_name = 'redmine.foo'
574 Setting.protocol = 'https'
574 Setting.protocol = 'https'
575
575
576 valid_languages.each do |lang|
576 valid_languages.each do |lang|
577 token.user.update_attribute :language, lang.to_s
577 token.user.update_attribute :language, lang.to_s
578 token.reload
578 token.reload
579 ActionMailer::Base.deliveries.clear
579 ActionMailer::Base.deliveries.clear
580 assert Mailer.register(token).deliver
580 assert Mailer.register(token).deliver
581 mail = last_email
581 mail = last_email
582 assert_select_email do
582 assert_select_email do
583 assert_select "a[href=?]",
583 assert_select "a[href=?]",
584 "https://redmine.foo/account/activate?token=#{token.value}",
584 "https://redmine.foo/account/activate?token=#{token.value}",
585 :text => "https://redmine.foo/account/activate?token=#{token.value}"
585 :text => "https://redmine.foo/account/activate?token=#{token.value}"
586 end
586 end
587 end
587 end
588 end
588 end
589
589
590 def test_test
590 def test_test
591 user = User.find(1)
591 user = User.find(1)
592 valid_languages.each do |lang|
592 valid_languages.each do |lang|
593 user.update_attribute :language, lang.to_s
593 user.update_attribute :language, lang.to_s
594 assert Mailer.test_email(user).deliver
594 assert Mailer.test_email(user).deliver
595 end
595 end
596 end
596 end
597
597
598 def test_reminders
598 def test_reminders
599 Mailer.reminders(:days => 42)
599 Mailer.reminders(:days => 42)
600 assert_equal 1, ActionMailer::Base.deliveries.size
600 assert_equal 1, ActionMailer::Base.deliveries.size
601 mail = last_email
601 mail = last_email
602 assert mail.bcc.include?('dlopper@somenet.foo')
602 assert mail.bcc.include?('dlopper@somenet.foo')
603 assert_mail_body_match 'Bug #3: Error 281 when updating a recipe', mail
603 assert_mail_body_match 'Bug #3: Error 281 when updating a recipe', mail
604 assert_equal '1 issue(s) due in the next 42 days', mail.subject
604 assert_equal '1 issue(s) due in the next 42 days', mail.subject
605 end
605 end
606
606
607 def test_reminders_should_not_include_closed_issues
607 def test_reminders_should_not_include_closed_issues
608 with_settings :default_language => 'en' do
608 with_settings :default_language => 'en' do
609 Issue.create!(:project_id => 1, :tracker_id => 1, :status_id => 5,
609 Issue.create!(:project_id => 1, :tracker_id => 1, :status_id => 5,
610 :subject => 'Closed issue', :assigned_to_id => 3,
610 :subject => 'Closed issue', :assigned_to_id => 3,
611 :due_date => 5.days.from_now,
611 :due_date => 5.days.from_now,
612 :author_id => 2)
612 :author_id => 2)
613 ActionMailer::Base.deliveries.clear
613 ActionMailer::Base.deliveries.clear
614
614
615 Mailer.reminders(:days => 42)
615 Mailer.reminders(:days => 42)
616 assert_equal 1, ActionMailer::Base.deliveries.size
616 assert_equal 1, ActionMailer::Base.deliveries.size
617 mail = last_email
617 mail = last_email
618 assert mail.bcc.include?('dlopper@somenet.foo')
618 assert mail.bcc.include?('dlopper@somenet.foo')
619 assert_mail_body_no_match 'Closed issue', mail
619 assert_mail_body_no_match 'Closed issue', mail
620 end
620 end
621 end
621 end
622
622
623 def test_reminders_for_users
623 def test_reminders_for_users
624 Mailer.reminders(:days => 42, :users => ['5'])
624 Mailer.reminders(:days => 42, :users => ['5'])
625 assert_equal 0, ActionMailer::Base.deliveries.size # No mail for dlopper
625 assert_equal 0, ActionMailer::Base.deliveries.size # No mail for dlopper
626 Mailer.reminders(:days => 42, :users => ['3'])
626 Mailer.reminders(:days => 42, :users => ['3'])
627 assert_equal 1, ActionMailer::Base.deliveries.size # No mail for dlopper
627 assert_equal 1, ActionMailer::Base.deliveries.size # No mail for dlopper
628 mail = last_email
628 mail = last_email
629 assert mail.bcc.include?('dlopper@somenet.foo')
629 assert mail.bcc.include?('dlopper@somenet.foo')
630 assert_mail_body_match 'Bug #3: Error 281 when updating a recipe', mail
630 assert_mail_body_match 'Bug #3: Error 281 when updating a recipe', mail
631 end
631 end
632
632
633 def test_reminder_should_include_issues_assigned_to_groups
633 def test_reminder_should_include_issues_assigned_to_groups
634 with_settings :default_language => 'en' do
634 with_settings :default_language => 'en' do
635 group = Group.generate!
635 group = Group.generate!
636 group.users << User.find(2)
636 group.users << User.find(2)
637 group.users << User.find(3)
637 group.users << User.find(3)
638
638
639 Issue.create!(:project_id => 1, :tracker_id => 1, :status_id => 1,
639 Issue.create!(:project_id => 1, :tracker_id => 1, :status_id => 1,
640 :subject => 'Assigned to group', :assigned_to => group,
640 :subject => 'Assigned to group', :assigned_to => group,
641 :due_date => 5.days.from_now,
641 :due_date => 5.days.from_now,
642 :author_id => 2)
642 :author_id => 2)
643 ActionMailer::Base.deliveries.clear
643 ActionMailer::Base.deliveries.clear
644
644
645 Mailer.reminders(:days => 7)
645 Mailer.reminders(:days => 7)
646 assert_equal 2, ActionMailer::Base.deliveries.size
646 assert_equal 2, ActionMailer::Base.deliveries.size
647 assert_equal %w(dlopper@somenet.foo jsmith@somenet.foo), ActionMailer::Base.deliveries.map(&:bcc).flatten.sort
647 assert_equal %w(dlopper@somenet.foo jsmith@somenet.foo), ActionMailer::Base.deliveries.map(&:bcc).flatten.sort
648 ActionMailer::Base.deliveries.each do |mail|
648 ActionMailer::Base.deliveries.each do |mail|
649 assert_mail_body_match 'Assigned to group', mail
649 assert_mail_body_match 'Assigned to group', mail
650 end
650 end
651 end
651 end
652 end
652 end
653
653
654 def test_reminders_with_version_option
654 def test_reminders_with_version_option
655 with_settings :default_language => 'en' do
655 with_settings :default_language => 'en' do
656 version = Version.generate!(:name => 'Acme', :project_id => 1)
656 version = Version.generate!(:name => 'Acme', :project_id => 1)
657 Issue.generate!(:assigned_to => User.find(2), :due_date => 5.days.from_now)
657 Issue.generate!(:assigned_to => User.find(2), :due_date => 5.days.from_now)
658 Issue.generate!(:assigned_to => User.find(3), :due_date => 5.days.from_now, :fixed_version => version)
658 Issue.generate!(:assigned_to => User.find(3), :due_date => 5.days.from_now, :fixed_version => version)
659 ActionMailer::Base.deliveries.clear
659 ActionMailer::Base.deliveries.clear
660
660
661 Mailer.reminders(:days => 42, :version => 'acme')
661 Mailer.reminders(:days => 42, :version => 'acme')
662 assert_equal 1, ActionMailer::Base.deliveries.size
662 assert_equal 1, ActionMailer::Base.deliveries.size
663
663
664 mail = last_email
664 mail = last_email
665 assert mail.bcc.include?('dlopper@somenet.foo')
665 assert mail.bcc.include?('dlopper@somenet.foo')
666 end
666 end
667 end
667 end
668
668
669 def test_mailer_should_not_change_locale
669 def test_mailer_should_not_change_locale
670 # Set current language to italian
670 # Set current language to italian
671 set_language_if_valid 'it'
671 set_language_if_valid 'it'
672 # Send an email to a french user
672 # Send an email to a french user
673 user = User.find(1)
673 user = User.find(1)
674 user.language = 'fr'
674 user.language = 'fr'
675 Mailer.account_activated(user).deliver
675 Mailer.account_activated(user).deliver
676 mail = last_email
676 mail = last_email
677 assert_mail_body_match 'Votre compte', mail
677 assert_mail_body_match 'Votre compte', mail
678
678
679 assert_equal :it, current_language
679 assert_equal :it, current_language
680 end
680 end
681
681
682 def test_with_deliveries_off
682 def test_with_deliveries_off
683 Mailer.with_deliveries false do
683 Mailer.with_deliveries false do
684 Mailer.test_email(User.find(1)).deliver
684 Mailer.test_email(User.find(1)).deliver
685 end
685 end
686 assert ActionMailer::Base.deliveries.empty?
686 assert ActionMailer::Base.deliveries.empty?
687 # should restore perform_deliveries
687 # should restore perform_deliveries
688 assert ActionMailer::Base.perform_deliveries
688 assert ActionMailer::Base.perform_deliveries
689 end
689 end
690
690
691 def test_token_for_should_strip_trailing_gt_from_address_with_full_name
691 def test_token_for_should_strip_trailing_gt_from_address_with_full_name
692 with_settings :mail_from => "Redmine Mailer<no-reply@redmine.org>" do
692 with_settings :mail_from => "Redmine Mailer<no-reply@redmine.org>" do
693 assert_match /\Aredmine.issue-\d+\.\d+\.[0-9a-f]+@redmine.org\z/, Mailer.token_for(Issue.generate!)
693 assert_match /\Aredmine.issue-\d+\.\d+\.[0-9a-f]+@redmine.org\z/, Mailer.token_for(Issue.generate!)
694 end
694 end
695 end
695 end
696
696
697 def test_layout_should_include_the_emails_header
697 def test_layout_should_include_the_emails_header
698 with_settings :emails_header => "*Header content*" do
698 with_settings :emails_header => "*Header content*" do
699 with_settings :plain_text_mail => 0 do
699 with_settings :plain_text_mail => 0 do
700 assert Mailer.test_email(User.find(1)).deliver
700 assert Mailer.test_email(User.find(1)).deliver
701 assert_select_email do
701 assert_select_email do
702 assert_select ".header" do
702 assert_select ".header" do
703 assert_select "strong", :text => "Header content"
703 assert_select "strong", :text => "Header content"
704 end
704 end
705 end
705 end
706 end
706 end
707 with_settings :plain_text_mail => 1 do
707 with_settings :plain_text_mail => 1 do
708 assert Mailer.test_email(User.find(1)).deliver
708 assert Mailer.test_email(User.find(1)).deliver
709 mail = last_email
709 mail = last_email
710 assert_not_nil mail
710 assert_not_nil mail
711 assert_include "*Header content*", mail.body.decoded
711 assert_include "*Header content*", mail.body.decoded
712 end
712 end
713 end
713 end
714 end
714 end
715
715
716 def test_layout_should_not_include_empty_emails_header
716 def test_layout_should_not_include_empty_emails_header
717 with_settings :emails_header => "", :plain_text_mail => 0 do
717 with_settings :emails_header => "", :plain_text_mail => 0 do
718 assert Mailer.test_email(User.find(1)).deliver
718 assert Mailer.test_email(User.find(1)).deliver
719 assert_select_email do
719 assert_select_email do
720 assert_select ".header", false
720 assert_select ".header", false
721 end
721 end
722 end
722 end
723 end
723 end
724
724
725 def test_layout_should_include_the_emails_footer
725 def test_layout_should_include_the_emails_footer
726 with_settings :emails_footer => "*Footer content*" do
726 with_settings :emails_footer => "*Footer content*" do
727 with_settings :plain_text_mail => 0 do
727 with_settings :plain_text_mail => 0 do
728 assert Mailer.test_email(User.find(1)).deliver
728 assert Mailer.test_email(User.find(1)).deliver
729 assert_select_email do
729 assert_select_email do
730 assert_select ".footer" do
730 assert_select ".footer" do
731 assert_select "strong", :text => "Footer content"
731 assert_select "strong", :text => "Footer content"
732 end
732 end
733 end
733 end
734 end
734 end
735 with_settings :plain_text_mail => 1 do
735 with_settings :plain_text_mail => 1 do
736 assert Mailer.test_email(User.find(1)).deliver
736 assert Mailer.test_email(User.find(1)).deliver
737 mail = last_email
737 mail = last_email
738 assert_not_nil mail
738 assert_not_nil mail
739 assert_include "\n-- \n", mail.body.decoded
739 assert_include "\n-- \n", mail.body.decoded
740 assert_include "*Footer content*", mail.body.decoded
740 assert_include "*Footer content*", mail.body.decoded
741 end
741 end
742 end
742 end
743 end
743 end
744
744
745 def test_layout_should_not_include_empty_emails_footer
745 def test_layout_should_not_include_empty_emails_footer
746 with_settings :emails_footer => "" do
746 with_settings :emails_footer => "" do
747 with_settings :plain_text_mail => 0 do
747 with_settings :plain_text_mail => 0 do
748 assert Mailer.test_email(User.find(1)).deliver
748 assert Mailer.test_email(User.find(1)).deliver
749 assert_select_email do
749 assert_select_email do
750 assert_select ".footer", false
750 assert_select ".footer", false
751 end
751 end
752 end
752 end
753 with_settings :plain_text_mail => 1 do
753 with_settings :plain_text_mail => 1 do
754 assert Mailer.test_email(User.find(1)).deliver
754 assert Mailer.test_email(User.find(1)).deliver
755 mail = last_email
755 mail = last_email
756 assert_not_nil mail
756 assert_not_nil mail
757 assert_not_include "\n-- \n", mail.body.decoded
757 assert_not_include "\n-- \n", mail.body.decoded
758 end
758 end
759 end
759 end
760 end
760 end
761
761
762 def test_should_escape_html_templates_only
762 def test_should_escape_html_templates_only
763 Issue.generate!(:project_id => 1, :tracker_id => 1, :subject => 'Subject with a <tag>')
763 Issue.generate!(:project_id => 1, :tracker_id => 1, :subject => 'Subject with a <tag>')
764 mail = last_email
764 mail = last_email
765 assert_equal 2, mail.parts.size
765 assert_equal 2, mail.parts.size
766 assert_include '<tag>', text_part.body.encoded
766 assert_include '<tag>', text_part.body.encoded
767 assert_include '&lt;tag&gt;', html_part.body.encoded
767 assert_include '&lt;tag&gt;', html_part.body.encoded
768 end
768 end
769
769
770 def test_should_raise_delivery_errors_when_raise_delivery_errors_is_true
770 def test_should_raise_delivery_errors_when_raise_delivery_errors_is_true
771 mail = Mailer.test_email(User.find(1))
771 mail = Mailer.test_email(User.find(1))
772 mail.delivery_method.stubs(:deliver!).raises(Exception.new("delivery error"))
772 mail.delivery_method.stubs(:deliver!).raises(Exception.new("delivery error"))
773
773
774 ActionMailer::Base.raise_delivery_errors = true
774 ActionMailer::Base.raise_delivery_errors = true
775 assert_raise Exception, "delivery error" do
775 assert_raise Exception, "delivery error" do
776 mail.deliver
776 mail.deliver
777 end
777 end
778 ensure
778 ensure
779 ActionMailer::Base.raise_delivery_errors = false
779 ActionMailer::Base.raise_delivery_errors = false
780 end
780 end
781
781
782 def test_should_log_delivery_errors_when_raise_delivery_errors_is_false
782 def test_should_log_delivery_errors_when_raise_delivery_errors_is_false
783 mail = Mailer.test_email(User.find(1))
783 mail = Mailer.test_email(User.find(1))
784 mail.delivery_method.stubs(:deliver!).raises(Exception.new("delivery error"))
784 mail.delivery_method.stubs(:deliver!).raises(Exception.new("delivery error"))
785
785
786 Rails.logger.expects(:error).with("Email delivery error: delivery error")
786 Rails.logger.expects(:error).with("Email delivery error: delivery error")
787 ActionMailer::Base.raise_delivery_errors = false
787 ActionMailer::Base.raise_delivery_errors = false
788 assert_nothing_raised do
788 assert_nothing_raised do
789 mail.deliver
789 mail.deliver
790 end
790 end
791 end
791 end
792
792
793 def test_with_synched_deliveries_should_yield_with_synced_deliveries
793 def test_with_synched_deliveries_should_yield_with_synced_deliveries
794 ActionMailer::Base.delivery_method = :async_smtp
794 ActionMailer::Base.delivery_method = :async_smtp
795 ActionMailer::Base.async_smtp_settings = {:foo => 'bar'}
795 ActionMailer::Base.async_smtp_settings = {:foo => 'bar'}
796
796
797 Mailer.with_synched_deliveries do
797 Mailer.with_synched_deliveries do
798 assert_equal :smtp, ActionMailer::Base.delivery_method
798 assert_equal :smtp, ActionMailer::Base.delivery_method
799 assert_equal({:foo => 'bar'}, ActionMailer::Base.smtp_settings)
799 assert_equal({:foo => 'bar'}, ActionMailer::Base.smtp_settings)
800 end
800 end
801 assert_equal :async_smtp, ActionMailer::Base.delivery_method
801 assert_equal :async_smtp, ActionMailer::Base.delivery_method
802 ensure
802 ensure
803 ActionMailer::Base.delivery_method = :test
803 ActionMailer::Base.delivery_method = :test
804 end
804 end
805
805
806 def test_email_addresses_should_keep_addresses
806 def test_email_addresses_should_keep_addresses
807 assert_equal ["foo@example.net"],
807 assert_equal ["foo@example.net"],
808 Mailer.email_addresses("foo@example.net")
808 Mailer.email_addresses("foo@example.net")
809
809
810 assert_equal ["foo@example.net", "bar@example.net"],
810 assert_equal ["foo@example.net", "bar@example.net"],
811 Mailer.email_addresses(["foo@example.net", "bar@example.net"])
811 Mailer.email_addresses(["foo@example.net", "bar@example.net"])
812 end
812 end
813
813
814 def test_email_addresses_should_replace_users_with_their_email_addresses
814 def test_email_addresses_should_replace_users_with_their_email_addresses
815 assert_equal ["admin@somenet.foo"],
815 assert_equal ["admin@somenet.foo"],
816 Mailer.email_addresses(User.find(1))
816 Mailer.email_addresses(User.find(1))
817
817
818 assert_equal ["admin@somenet.foo", "jsmith@somenet.foo"],
818 assert_equal ["admin@somenet.foo", "jsmith@somenet.foo"],
819 Mailer.email_addresses(User.where(:id => [1,2])).sort
819 Mailer.email_addresses(User.where(:id => [1,2])).sort
820 end
820 end
821
821
822 def test_email_addresses_should_include_notified_emails_addresses_only
822 def test_email_addresses_should_include_notified_emails_addresses_only
823 EmailAddress.create!(:user_id => 2, :address => "another@somenet.foo", :notify => false)
823 EmailAddress.create!(:user_id => 2, :address => "another@somenet.foo", :notify => false)
824 EmailAddress.create!(:user_id => 2, :address => "another2@somenet.foo")
824 EmailAddress.create!(:user_id => 2, :address => "another2@somenet.foo")
825
825
826 assert_equal ["another2@somenet.foo", "jsmith@somenet.foo"],
826 assert_equal ["another2@somenet.foo", "jsmith@somenet.foo"],
827 Mailer.email_addresses(User.find(2)).sort
827 Mailer.email_addresses(User.find(2)).sort
828 end
828 end
829
829
830 private
830 private
831
831
832 def last_email
832 def last_email
833 mail = ActionMailer::Base.deliveries.last
833 mail = ActionMailer::Base.deliveries.last
834 assert_not_nil mail
834 assert_not_nil mail
835 mail
835 mail
836 end
836 end
837
837
838 def text_part
838 def text_part
839 last_email.parts.detect {|part| part.content_type.include?('text/plain')}
839 last_email.parts.detect {|part| part.content_type.include?('text/plain')}
840 end
840 end
841
841
842 def html_part
842 def html_part
843 last_email.parts.detect {|part| part.content_type.include?('text/html')}
843 last_email.parts.detect {|part| part.content_type.include?('text/html')}
844 end
844 end
845
845
846 def with_each_language_as_default(&block)
846 def with_each_language_as_default(&block)
847 valid_languages.each do |lang|
847 valid_languages.each do |lang|
848 with_settings :default_language => lang.to_s do
848 with_settings :default_language => lang.to_s do
849 yield lang
849 yield lang
850 end
850 end
851 end
851 end
852 end
852 end
853 end
853 end
General Comments 0
You need to be logged in to leave comments. Login now