##// END OF EJS Templates
Fixed unsafe call to #casecmp (#20369, #21000)....
Jean-Philippe Lang -
r14294:bae4b1985d92
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 && 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.tracker.name}: #{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.4', 'application')
1268 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.4', '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,1534
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 => 'Bug: 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 => 'Bug: 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 => 'Bug: 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 => "Bug: 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_unbalanced_closing_pre_tag_should_not_error
998 assert_nothing_raised do
999 textilizable("unbalanced</pre>")
1000 end
1001 end
1002
997 def test_syntax_highlight
1003 def test_syntax_highlight
998 raw = <<-RAW
1004 raw = <<-RAW
999 <pre><code class="ruby">
1005 <pre><code class="ruby">
1000 # Some ruby code here
1006 # Some ruby code here
1001 </code></pre>
1007 </code></pre>
1002 RAW
1008 RAW
1003
1009
1004 expected = <<-EXPECTED
1010 expected = <<-EXPECTED
1005 <pre><code class="ruby syntaxhl"><span class=\"CodeRay\"><span class="comment"># Some ruby code here</span></span>
1011 <pre><code class="ruby syntaxhl"><span class=\"CodeRay\"><span class="comment"># Some ruby code here</span></span>
1006 </code></pre>
1012 </code></pre>
1007 EXPECTED
1013 EXPECTED
1008
1014
1009 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
1015 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
1010 end
1016 end
1011
1017
1012 def test_to_path_param
1018 def test_to_path_param
1013 assert_equal 'test1/test2', to_path_param('test1/test2')
1019 assert_equal 'test1/test2', to_path_param('test1/test2')
1014 assert_equal 'test1/test2', to_path_param('/test1/test2/')
1020 assert_equal 'test1/test2', to_path_param('/test1/test2/')
1015 assert_equal 'test1/test2', to_path_param('//test1/test2/')
1021 assert_equal 'test1/test2', to_path_param('//test1/test2/')
1016 assert_equal nil, to_path_param('/')
1022 assert_equal nil, to_path_param('/')
1017 end
1023 end
1018
1024
1019 def test_wiki_links_in_tables
1025 def test_wiki_links_in_tables
1020 text = "|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|"
1026 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")
1027 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")
1028 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")
1029 link3 = link_to("Last page", "/projects/ecookbook/wiki/Last_page", :class => "wiki-page new")
1024 result = "<tr><td>#{link1}</td>" +
1030 result = "<tr><td>#{link1}</td>" +
1025 "<td>#{link2}</td>" +
1031 "<td>#{link2}</td>" +
1026 "</tr><tr><td>Cell 21</td><td>#{link3}</td></tr>"
1032 "</tr><tr><td>Cell 21</td><td>#{link3}</td></tr>"
1027 @project = Project.find(1)
1033 @project = Project.find(1)
1028 assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '')
1034 assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '')
1029 end
1035 end
1030
1036
1031 def test_text_formatting
1037 def test_text_formatting
1032 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
1038 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>)',
1039 '(_text within parentheses_)' => '(<em>text within parentheses</em>)',
1034 'a *Humane Web* Text Generator' => 'a <strong>Humane Web</strong> Text Generator',
1040 '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>',
1041 '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',
1042 '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 }
1043 }
1038 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
1044 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
1039 end
1045 end
1040
1046
1041 def test_wiki_horizontal_rule
1047 def test_wiki_horizontal_rule
1042 assert_equal '<hr />', textilizable('---')
1048 assert_equal '<hr />', textilizable('---')
1043 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
1049 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
1044 end
1050 end
1045
1051
1046 def test_footnotes
1052 def test_footnotes
1047 raw = <<-RAW
1053 raw = <<-RAW
1048 This is some text[1].
1054 This is some text[1].
1049
1055
1050 fn1. This is the foot note
1056 fn1. This is the foot note
1051 RAW
1057 RAW
1052
1058
1053 expected = <<-EXPECTED
1059 expected = <<-EXPECTED
1054 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
1060 <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>
1061 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
1056 EXPECTED
1062 EXPECTED
1057
1063
1058 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
1064 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
1059 end
1065 end
1060
1066
1061 def test_headings
1067 def test_headings
1062 raw = 'h1. Some heading'
1068 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>|
1069 expected = %|<a name="Some-heading"></a>\n<h1 >Some heading<a href="#Some-heading" class="wiki-anchor">&para;</a></h1>|
1064
1070
1065 assert_equal expected, textilizable(raw)
1071 assert_equal expected, textilizable(raw)
1066 end
1072 end
1067
1073
1068 def test_headings_with_special_chars
1074 def test_headings_with_special_chars
1069 # This test makes sure that the generated anchor names match the expected
1075 # This test makes sure that the generated anchor names match the expected
1070 # ones even if the heading text contains unconventional characters
1076 # ones even if the heading text contains unconventional characters
1071 raw = 'h1. Some heading related to version 0.5'
1077 raw = 'h1. Some heading related to version 0.5'
1072 anchor = sanitize_anchor_name("Some-heading-related-to-version-0.5")
1078 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>|
1079 expected = %|<a name="#{anchor}"></a>\n<h1 >Some heading related to version 0.5<a href="##{anchor}" class="wiki-anchor">&para;</a></h1>|
1074
1080
1075 assert_equal expected, textilizable(raw)
1081 assert_equal expected, textilizable(raw)
1076 end
1082 end
1077
1083
1078 def test_headings_in_wiki_single_page_export_should_be_prepended_with_page_title
1084 def test_headings_in_wiki_single_page_export_should_be_prepended_with_page_title
1079 page = WikiPage.new( :title => 'Page Title', :wiki_id => 1 )
1085 page = WikiPage.new( :title => 'Page Title', :wiki_id => 1 )
1080 content = WikiContent.new( :text => 'h1. Some heading', :page => page )
1086 content = WikiContent.new( :text => 'h1. Some heading', :page => page )
1081
1087
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>|
1088 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
1089
1084 assert_equal expected, textilizable(content, :text, :wiki_links => :anchor )
1090 assert_equal expected, textilizable(content, :text, :wiki_links => :anchor )
1085 end
1091 end
1086
1092
1087 def test_table_of_content
1093 def test_table_of_content
1088 raw = <<-RAW
1094 raw = <<-RAW
1089 {{toc}}
1095 {{toc}}
1090
1096
1091 h1. Title
1097 h1. Title
1092
1098
1093 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
1099 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
1094
1100
1095 h2. Subtitle with a [[Wiki]] link
1101 h2. Subtitle with a [[Wiki]] link
1096
1102
1097 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
1103 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
1098
1104
1099 h2. Subtitle with [[Wiki|another Wiki]] link
1105 h2. Subtitle with [[Wiki|another Wiki]] link
1100
1106
1101 h2. Subtitle with %{color:red}red text%
1107 h2. Subtitle with %{color:red}red text%
1102
1108
1103 <pre>
1109 <pre>
1104 some code
1110 some code
1105 </pre>
1111 </pre>
1106
1112
1107 h3. Subtitle with *some* _modifiers_
1113 h3. Subtitle with *some* _modifiers_
1108
1114
1109 h3. Subtitle with @inline code@
1115 h3. Subtitle with @inline code@
1110
1116
1111 h1. Another title
1117 h1. Another title
1112
1118
1113 h3. An "Internet link":http://www.redmine.org/ inside subtitle
1119 h3. An "Internet link":http://www.redmine.org/ inside subtitle
1114
1120
1115 h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
1121 h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
1116
1122
1117 RAW
1123 RAW
1118
1124
1119 expected = '<ul class="toc">' +
1125 expected = '<ul class="toc">' +
1120 '<li><a href="#Title">Title</a>' +
1126 '<li><a href="#Title">Title</a>' +
1121 '<ul>' +
1127 '<ul>' +
1122 '<li><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
1128 '<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>' +
1129 '<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>' +
1130 '<li><a href="#Subtitle-with-red-text">Subtitle with red text</a>' +
1125 '<ul>' +
1131 '<ul>' +
1126 '<li><a href="#Subtitle-with-some-modifiers">Subtitle with some modifiers</a></li>' +
1132 '<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>' +
1133 '<li><a href="#Subtitle-with-inline-code">Subtitle with inline code</a></li>' +
1128 '</ul>' +
1134 '</ul>' +
1129 '</li>' +
1135 '</li>' +
1130 '</ul>' +
1136 '</ul>' +
1131 '</li>' +
1137 '</li>' +
1132 '<li><a href="#Another-title">Another title</a>' +
1138 '<li><a href="#Another-title">Another title</a>' +
1133 '<ul>' +
1139 '<ul>' +
1134 '<li>' +
1140 '<li>' +
1135 '<ul>' +
1141 '<ul>' +
1136 '<li><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
1142 '<li><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
1137 '</ul>' +
1143 '</ul>' +
1138 '</li>' +
1144 '</li>' +
1139 '<li><a href="#Project-Name">Project Name</a></li>' +
1145 '<li><a href="#Project-Name">Project Name</a></li>' +
1140 '</ul>' +
1146 '</ul>' +
1141 '</li>' +
1147 '</li>' +
1142 '</ul>'
1148 '</ul>'
1143
1149
1144 @project = Project.find(1)
1150 @project = Project.find(1)
1145 assert textilizable(raw).gsub("\n", "").include?(expected)
1151 assert textilizable(raw).gsub("\n", "").include?(expected)
1146 end
1152 end
1147
1153
1148 def test_table_of_content_should_generate_unique_anchors
1154 def test_table_of_content_should_generate_unique_anchors
1149 raw = <<-RAW
1155 raw = <<-RAW
1150 {{toc}}
1156 {{toc}}
1151
1157
1152 h1. Title
1158 h1. Title
1153
1159
1154 h2. Subtitle
1160 h2. Subtitle
1155
1161
1156 h2. Subtitle
1162 h2. Subtitle
1157 RAW
1163 RAW
1158
1164
1159 expected = '<ul class="toc">' +
1165 expected = '<ul class="toc">' +
1160 '<li><a href="#Title">Title</a>' +
1166 '<li><a href="#Title">Title</a>' +
1161 '<ul>' +
1167 '<ul>' +
1162 '<li><a href="#Subtitle">Subtitle</a></li>' +
1168 '<li><a href="#Subtitle">Subtitle</a></li>' +
1163 '<li><a href="#Subtitle-2">Subtitle</a></li>'
1169 '<li><a href="#Subtitle-2">Subtitle</a></li>'
1164 '</ul>'
1170 '</ul>'
1165 '</li>' +
1171 '</li>' +
1166 '</ul>'
1172 '</ul>'
1167
1173
1168 @project = Project.find(1)
1174 @project = Project.find(1)
1169 result = textilizable(raw).gsub("\n", "")
1175 result = textilizable(raw).gsub("\n", "")
1170 assert_include expected, result
1176 assert_include expected, result
1171 assert_include '<a name="Subtitle">', result
1177 assert_include '<a name="Subtitle">', result
1172 assert_include '<a name="Subtitle-2">', result
1178 assert_include '<a name="Subtitle-2">', result
1173 end
1179 end
1174
1180
1175 def test_table_of_content_should_contain_included_page_headings
1181 def test_table_of_content_should_contain_included_page_headings
1176 raw = <<-RAW
1182 raw = <<-RAW
1177 {{toc}}
1183 {{toc}}
1178
1184
1179 h1. Included
1185 h1. Included
1180
1186
1181 {{include(Child_1)}}
1187 {{include(Child_1)}}
1182 RAW
1188 RAW
1183
1189
1184 expected = '<ul class="toc">' +
1190 expected = '<ul class="toc">' +
1185 '<li><a href="#Included">Included</a></li>' +
1191 '<li><a href="#Included">Included</a></li>' +
1186 '<li><a href="#Child-page-1">Child page 1</a></li>' +
1192 '<li><a href="#Child-page-1">Child page 1</a></li>' +
1187 '</ul>'
1193 '</ul>'
1188
1194
1189 @project = Project.find(1)
1195 @project = Project.find(1)
1190 assert textilizable(raw).gsub("\n", "").include?(expected)
1196 assert textilizable(raw).gsub("\n", "").include?(expected)
1191 end
1197 end
1192
1198
1193 def test_toc_with_textile_formatting_should_be_parsed
1199 def test_toc_with_textile_formatting_should_be_parsed
1194 with_settings :text_formatting => 'textile' do
1200 with_settings :text_formatting => 'textile' do
1195 assert_select_in textilizable("{{toc}}\n\nh1. Heading"), 'ul.toc li', :text => 'Heading'
1201 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'
1202 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'
1203 assert_select_in textilizable("{{>toc}}\n\nh1. Heading"), 'ul.toc.right li', :text => 'Heading'
1198 end
1204 end
1199 end
1205 end
1200
1206
1201 if Object.const_defined?(:Redcarpet)
1207 if Object.const_defined?(:Redcarpet)
1202 def test_toc_with_markdown_formatting_should_be_parsed
1208 def test_toc_with_markdown_formatting_should_be_parsed
1203 with_settings :text_formatting => 'markdown' do
1209 with_settings :text_formatting => 'markdown' do
1204 assert_select_in textilizable("{{toc}}\n\n# Heading"), 'ul.toc li', :text => 'Heading'
1210 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'
1211 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'
1212 assert_select_in textilizable("{{>toc}}\n\n# Heading"), 'ul.toc.right li', :text => 'Heading'
1207 end
1213 end
1208 end
1214 end
1209 end
1215 end
1210
1216
1211 def test_section_edit_links
1217 def test_section_edit_links
1212 raw = <<-RAW
1218 raw = <<-RAW
1213 h1. Title
1219 h1. Title
1214
1220
1215 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
1221 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
1216
1222
1217 h2. Subtitle with a [[Wiki]] link
1223 h2. Subtitle with a [[Wiki]] link
1218
1224
1219 h2. Subtitle with *some* _modifiers_
1225 h2. Subtitle with *some* _modifiers_
1220
1226
1221 h2. Subtitle with @inline code@
1227 h2. Subtitle with @inline code@
1222
1228
1223 <pre>
1229 <pre>
1224 some code
1230 some code
1225
1231
1226 h2. heading inside pre
1232 h2. heading inside pre
1227
1233
1228 <h2>html heading inside pre</h2>
1234 <h2>html heading inside pre</h2>
1229 </pre>
1235 </pre>
1230
1236
1231 h2. Subtitle after pre tag
1237 h2. Subtitle after pre tag
1232 RAW
1238 RAW
1233
1239
1234 @project = Project.find(1)
1240 @project = Project.find(1)
1235 set_language_if_valid 'en'
1241 set_language_if_valid 'en'
1236 result = textilizable(raw, :edit_section_links => {:controller => 'wiki', :action => 'edit', :project_id => '1', :id => 'Test'}).gsub("\n", "")
1242 result = textilizable(raw, :edit_section_links => {:controller => 'wiki', :action => 'edit', :project_id => '1', :id => 'Test'}).gsub("\n", "")
1237
1243
1238 # heading that contains inline code
1244 # heading that contains inline code
1239 assert_match Regexp.new('<div class="contextual" title="Edit this section" id="section-4">' +
1245 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>' +
1246 '<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>' +
1247 '<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>'),
1248 '<h2 >Subtitle with <code>inline code</code><a href="#Subtitle-with-inline-code" class="wiki-anchor">&para;</a></h2>'),
1243 result
1249 result
1244
1250
1245 # last heading
1251 # last heading
1246 assert_match Regexp.new('<div class="contextual" title="Edit this section" id="section-5">' +
1252 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>' +
1253 '<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>' +
1254 '<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>'),
1255 '<h2 >Subtitle after pre tag<a href="#Subtitle-after-pre-tag" class="wiki-anchor">&para;</a></h2>'),
1250 result
1256 result
1251 end
1257 end
1252
1258
1253 def test_default_formatter
1259 def test_default_formatter
1254 with_settings :text_formatting => 'unknown' do
1260 with_settings :text_formatting => 'unknown' do
1255 text = 'a *link*: http://www.example.net/'
1261 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)
1262 assert_equal '<p>a *link*: <a class="external" href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
1257 end
1263 end
1258 end
1264 end
1259
1265
1260 def test_due_date_distance_in_words
1266 def test_due_date_distance_in_words
1261 to_test = { Date.today => 'Due in 0 days',
1267 to_test = { Date.today => 'Due in 0 days',
1262 Date.today + 1 => 'Due in 1 day',
1268 Date.today + 1 => 'Due in 1 day',
1263 Date.today + 100 => 'Due in about 3 months',
1269 Date.today + 100 => 'Due in about 3 months',
1264 Date.today + 20000 => 'Due in over 54 years',
1270 Date.today + 20000 => 'Due in over 54 years',
1265 Date.today - 1 => '1 day late',
1271 Date.today - 1 => '1 day late',
1266 Date.today - 100 => 'about 3 months late',
1272 Date.today - 100 => 'about 3 months late',
1267 Date.today - 20000 => 'over 54 years late',
1273 Date.today - 20000 => 'over 54 years late',
1268 }
1274 }
1269 ::I18n.locale = :en
1275 ::I18n.locale = :en
1270 to_test.each do |date, expected|
1276 to_test.each do |date, expected|
1271 assert_equal expected, due_date_distance_in_words(date)
1277 assert_equal expected, due_date_distance_in_words(date)
1272 end
1278 end
1273 end
1279 end
1274
1280
1275 def test_avatar_enabled
1281 def test_avatar_enabled
1276 with_settings :gravatar_enabled => '1' do
1282 with_settings :gravatar_enabled => '1' do
1277 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
1283 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'))
1284 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
1279 # Default size is 50
1285 # Default size is 50
1280 assert avatar('jsmith <jsmith@somenet.foo>').include?('size=50')
1286 assert avatar('jsmith <jsmith@somenet.foo>').include?('size=50')
1281 assert avatar('jsmith <jsmith@somenet.foo>', :size => 24).include?('size=24')
1287 assert avatar('jsmith <jsmith@somenet.foo>', :size => 24).include?('size=24')
1282 # Non-avatar options should be considered html options
1288 # Non-avatar options should be considered html options
1283 assert avatar('jsmith <jsmith@somenet.foo>', :title => 'John Smith').include?('title="John Smith"')
1289 assert avatar('jsmith <jsmith@somenet.foo>', :title => 'John Smith').include?('title="John Smith"')
1284 # The default class of the img tag should be gravatar
1290 # The default class of the img tag should be gravatar
1285 assert avatar('jsmith <jsmith@somenet.foo>').include?('class="gravatar"')
1291 assert avatar('jsmith <jsmith@somenet.foo>').include?('class="gravatar"')
1286 assert !avatar('jsmith <jsmith@somenet.foo>', :class => 'picture').include?('class="gravatar"')
1292 assert !avatar('jsmith <jsmith@somenet.foo>', :class => 'picture').include?('class="gravatar"')
1287 assert_nil avatar('jsmith')
1293 assert_nil avatar('jsmith')
1288 assert_nil avatar(nil)
1294 assert_nil avatar(nil)
1289 end
1295 end
1290 end
1296 end
1291
1297
1292 def test_avatar_disabled
1298 def test_avatar_disabled
1293 with_settings :gravatar_enabled => '0' do
1299 with_settings :gravatar_enabled => '0' do
1294 assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
1300 assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
1295 end
1301 end
1296 end
1302 end
1297
1303
1298 def test_link_to_user
1304 def test_link_to_user
1299 user = User.find(2)
1305 user = User.find(2)
1300 result = link_to("John Smith", "/users/2", :class => "user active")
1306 result = link_to("John Smith", "/users/2", :class => "user active")
1301 assert_equal result, link_to_user(user)
1307 assert_equal result, link_to_user(user)
1302 end
1308 end
1303
1309
1304 def test_link_to_user_should_not_link_to_locked_user
1310 def test_link_to_user_should_not_link_to_locked_user
1305 with_current_user nil do
1311 with_current_user nil do
1306 user = User.find(5)
1312 user = User.find(5)
1307 assert user.locked?
1313 assert user.locked?
1308 assert_equal 'Dave2 Lopper2', link_to_user(user)
1314 assert_equal 'Dave2 Lopper2', link_to_user(user)
1309 end
1315 end
1310 end
1316 end
1311
1317
1312 def test_link_to_user_should_link_to_locked_user_if_current_user_is_admin
1318 def test_link_to_user_should_link_to_locked_user_if_current_user_is_admin
1313 with_current_user User.find(1) do
1319 with_current_user User.find(1) do
1314 user = User.find(5)
1320 user = User.find(5)
1315 assert user.locked?
1321 assert user.locked?
1316 result = link_to("Dave2 Lopper2", "/users/5", :class => "user locked")
1322 result = link_to("Dave2 Lopper2", "/users/5", :class => "user locked")
1317 assert_equal result, link_to_user(user)
1323 assert_equal result, link_to_user(user)
1318 end
1324 end
1319 end
1325 end
1320
1326
1321 def test_link_to_user_should_not_link_to_anonymous
1327 def test_link_to_user_should_not_link_to_anonymous
1322 user = User.anonymous
1328 user = User.anonymous
1323 assert user.anonymous?
1329 assert user.anonymous?
1324 t = link_to_user(user)
1330 t = link_to_user(user)
1325 assert_equal ::I18n.t(:label_user_anonymous), t
1331 assert_equal ::I18n.t(:label_user_anonymous), t
1326 end
1332 end
1327
1333
1328 def test_link_to_attachment
1334 def test_link_to_attachment
1329 a = Attachment.find(3)
1335 a = Attachment.find(3)
1330 assert_equal '<a href="/attachments/3/logo.gif">logo.gif</a>',
1336 assert_equal '<a href="/attachments/3/logo.gif">logo.gif</a>',
1331 link_to_attachment(a)
1337 link_to_attachment(a)
1332 assert_equal '<a href="/attachments/3/logo.gif">Text</a>',
1338 assert_equal '<a href="/attachments/3/logo.gif">Text</a>',
1333 link_to_attachment(a, :text => 'Text')
1339 link_to_attachment(a, :text => 'Text')
1334 result = link_to("logo.gif", "/attachments/3/logo.gif", :class => "foo")
1340 result = link_to("logo.gif", "/attachments/3/logo.gif", :class => "foo")
1335 assert_equal result,
1341 assert_equal result,
1336 link_to_attachment(a, :class => 'foo')
1342 link_to_attachment(a, :class => 'foo')
1337 assert_equal '<a href="/attachments/download/3/logo.gif">logo.gif</a>',
1343 assert_equal '<a href="/attachments/download/3/logo.gif">logo.gif</a>',
1338 link_to_attachment(a, :download => true)
1344 link_to_attachment(a, :download => true)
1339 assert_equal '<a href="http://test.host/attachments/3/logo.gif">logo.gif</a>',
1345 assert_equal '<a href="http://test.host/attachments/3/logo.gif">logo.gif</a>',
1340 link_to_attachment(a, :only_path => false)
1346 link_to_attachment(a, :only_path => false)
1341 end
1347 end
1342
1348
1343 def test_thumbnail_tag
1349 def test_thumbnail_tag
1344 a = Attachment.find(3)
1350 a = Attachment.find(3)
1345 assert_select_in thumbnail_tag(a),
1351 assert_select_in thumbnail_tag(a),
1346 'a[href=?][title=?] img[alt="3"][src=?]',
1352 'a[href=?][title=?] img[alt="3"][src=?]',
1347 "/attachments/3/logo.gif", "logo.gif", "/attachments/thumbnail/3"
1353 "/attachments/3/logo.gif", "logo.gif", "/attachments/thumbnail/3"
1348 end
1354 end
1349
1355
1350 def test_link_to_project
1356 def test_link_to_project
1351 project = Project.find(1)
1357 project = Project.find(1)
1352 assert_equal %(<a href="/projects/ecookbook">eCookbook</a>),
1358 assert_equal %(<a href="/projects/ecookbook">eCookbook</a>),
1353 link_to_project(project)
1359 link_to_project(project)
1354 assert_equal %(<a href="http://test.host/projects/ecookbook?jump=blah">eCookbook</a>),
1360 assert_equal %(<a href="http://test.host/projects/ecookbook?jump=blah">eCookbook</a>),
1355 link_to_project(project, {:only_path => false, :jump => 'blah'})
1361 link_to_project(project, {:only_path => false, :jump => 'blah'})
1356 end
1362 end
1357
1363
1358 def test_link_to_project_settings
1364 def test_link_to_project_settings
1359 project = Project.find(1)
1365 project = Project.find(1)
1360 assert_equal '<a href="/projects/ecookbook/settings">eCookbook</a>', link_to_project_settings(project)
1366 assert_equal '<a href="/projects/ecookbook/settings">eCookbook</a>', link_to_project_settings(project)
1361
1367
1362 project.status = Project::STATUS_CLOSED
1368 project.status = Project::STATUS_CLOSED
1363 assert_equal '<a href="/projects/ecookbook">eCookbook</a>', link_to_project_settings(project)
1369 assert_equal '<a href="/projects/ecookbook">eCookbook</a>', link_to_project_settings(project)
1364
1370
1365 project.status = Project::STATUS_ARCHIVED
1371 project.status = Project::STATUS_ARCHIVED
1366 assert_equal 'eCookbook', link_to_project_settings(project)
1372 assert_equal 'eCookbook', link_to_project_settings(project)
1367 end
1373 end
1368
1374
1369 def test_link_to_legacy_project_with_numerical_identifier_should_use_id
1375 def test_link_to_legacy_project_with_numerical_identifier_should_use_id
1370 # numeric identifier are no longer allowed
1376 # numeric identifier are no longer allowed
1371 Project.where(:id => 1).update_all(:identifier => 25)
1377 Project.where(:id => 1).update_all(:identifier => 25)
1372 assert_equal '<a href="/projects/1">eCookbook</a>',
1378 assert_equal '<a href="/projects/1">eCookbook</a>',
1373 link_to_project(Project.find(1))
1379 link_to_project(Project.find(1))
1374 end
1380 end
1375
1381
1376 def test_principals_options_for_select_with_users
1382 def test_principals_options_for_select_with_users
1377 User.current = nil
1383 User.current = nil
1378 users = [User.find(2), User.find(4)]
1384 users = [User.find(2), User.find(4)]
1379 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>),
1385 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>),
1380 principals_options_for_select(users)
1386 principals_options_for_select(users)
1381 end
1387 end
1382
1388
1383 def test_principals_options_for_select_with_selected
1389 def test_principals_options_for_select_with_selected
1384 User.current = nil
1390 User.current = nil
1385 users = [User.find(2), User.find(4)]
1391 users = [User.find(2), User.find(4)]
1386 assert_equal %(<option value="2">John Smith</option><option value="4" selected="selected">Robert Hill</option>),
1392 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))
1393 principals_options_for_select(users, User.find(4))
1388 end
1394 end
1389
1395
1390 def test_principals_options_for_select_with_users_and_groups
1396 def test_principals_options_for_select_with_users_and_groups
1391 User.current = nil
1397 User.current = nil
1392 set_language_if_valid 'en'
1398 set_language_if_valid 'en'
1393 users = [User.find(2), Group.find(11), User.find(4), Group.find(10)]
1399 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>) +
1400 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>),
1401 %(<optgroup label="Groups"><option value="10">A Team</option><option value="11">B Team</option></optgroup>),
1396 principals_options_for_select(users)
1402 principals_options_for_select(users)
1397 end
1403 end
1398
1404
1399 def test_principals_options_for_select_with_empty_collection
1405 def test_principals_options_for_select_with_empty_collection
1400 assert_equal '', principals_options_for_select([])
1406 assert_equal '', principals_options_for_select([])
1401 end
1407 end
1402
1408
1403 def test_principals_options_for_select_should_include_me_option_when_current_user_is_in_collection
1409 def test_principals_options_for_select_should_include_me_option_when_current_user_is_in_collection
1404 set_language_if_valid 'en'
1410 set_language_if_valid 'en'
1405 users = [User.find(2), User.find(4)]
1411 users = [User.find(2), User.find(4)]
1406 User.current = User.find(4)
1412 User.current = User.find(4)
1407 assert_include '<option value="4">&lt;&lt; me &gt;&gt;</option>', principals_options_for_select(users)
1413 assert_include '<option value="4">&lt;&lt; me &gt;&gt;</option>', principals_options_for_select(users)
1408 end
1414 end
1409
1415
1410 def test_stylesheet_link_tag_should_pick_the_default_stylesheet
1416 def test_stylesheet_link_tag_should_pick_the_default_stylesheet
1411 assert_match 'href="/stylesheets/styles.css"', stylesheet_link_tag("styles")
1417 assert_match 'href="/stylesheets/styles.css"', stylesheet_link_tag("styles")
1412 end
1418 end
1413
1419
1414 def test_stylesheet_link_tag_for_plugin_should_pick_the_plugin_stylesheet
1420 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)
1421 assert_match 'href="/plugin_assets/foo/stylesheets/styles.css"', stylesheet_link_tag("styles", :plugin => :foo)
1416 end
1422 end
1417
1423
1418 def test_image_tag_should_pick_the_default_image
1424 def test_image_tag_should_pick_the_default_image
1419 assert_match 'src="/images/image.png"', image_tag("image.png")
1425 assert_match 'src="/images/image.png"', image_tag("image.png")
1420 end
1426 end
1421
1427
1422 def test_image_tag_should_pick_the_theme_image_if_it_exists
1428 def test_image_tag_should_pick_the_theme_image_if_it_exists
1423 theme = Redmine::Themes.themes.last
1429 theme = Redmine::Themes.themes.last
1424 theme.images << 'image.png'
1430 theme.images << 'image.png'
1425
1431
1426 with_settings :ui_theme => theme.id do
1432 with_settings :ui_theme => theme.id do
1427 assert_match %|src="/themes/#{theme.dir}/images/image.png"|, image_tag("image.png")
1433 assert_match %|src="/themes/#{theme.dir}/images/image.png"|, image_tag("image.png")
1428 assert_match %|src="/images/other.png"|, image_tag("other.png")
1434 assert_match %|src="/images/other.png"|, image_tag("other.png")
1429 end
1435 end
1430 ensure
1436 ensure
1431 theme.images.delete 'image.png'
1437 theme.images.delete 'image.png'
1432 end
1438 end
1433
1439
1434 def test_image_tag_sfor_plugin_should_pick_the_plugin_image
1440 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)
1441 assert_match 'src="/plugin_assets/foo/images/image.png"', image_tag("image.png", :plugin => :foo)
1436 end
1442 end
1437
1443
1438 def test_javascript_include_tag_should_pick_the_default_javascript
1444 def test_javascript_include_tag_should_pick_the_default_javascript
1439 assert_match 'src="/javascripts/scripts.js"', javascript_include_tag("scripts")
1445 assert_match 'src="/javascripts/scripts.js"', javascript_include_tag("scripts")
1440 end
1446 end
1441
1447
1442 def test_javascript_include_tag_for_plugin_should_pick_the_plugin_javascript
1448 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)
1449 assert_match 'src="/plugin_assets/foo/javascripts/scripts.js"', javascript_include_tag("scripts", :plugin => :foo)
1444 end
1450 end
1445
1451
1446 def test_raw_json_should_escape_closing_tags
1452 def test_raw_json_should_escape_closing_tags
1447 s = raw_json(["<foo>bar</foo>"])
1453 s = raw_json(["<foo>bar</foo>"])
1448 assert_include '\/foo', s
1454 assert_include '\/foo', s
1449 end
1455 end
1450
1456
1451 def test_raw_json_should_be_html_safe
1457 def test_raw_json_should_be_html_safe
1452 s = raw_json(["foo"])
1458 s = raw_json(["foo"])
1453 assert s.html_safe?
1459 assert s.html_safe?
1454 end
1460 end
1455
1461
1456 def test_html_title_should_app_title_if_not_set
1462 def test_html_title_should_app_title_if_not_set
1457 assert_equal 'Redmine', html_title
1463 assert_equal 'Redmine', html_title
1458 end
1464 end
1459
1465
1460 def test_html_title_should_join_items
1466 def test_html_title_should_join_items
1461 html_title 'Foo', 'Bar'
1467 html_title 'Foo', 'Bar'
1462 assert_equal 'Foo - Bar - Redmine', html_title
1468 assert_equal 'Foo - Bar - Redmine', html_title
1463 end
1469 end
1464
1470
1465 def test_html_title_should_append_current_project_name
1471 def test_html_title_should_append_current_project_name
1466 @project = Project.find(1)
1472 @project = Project.find(1)
1467 html_title 'Foo', 'Bar'
1473 html_title 'Foo', 'Bar'
1468 assert_equal 'Foo - Bar - eCookbook - Redmine', html_title
1474 assert_equal 'Foo - Bar - eCookbook - Redmine', html_title
1469 end
1475 end
1470
1476
1471 def test_title_should_return_a_h2_tag
1477 def test_title_should_return_a_h2_tag
1472 assert_equal '<h2>Foo</h2>', title('Foo')
1478 assert_equal '<h2>Foo</h2>', title('Foo')
1473 end
1479 end
1474
1480
1475 def test_title_should_set_html_title
1481 def test_title_should_set_html_title
1476 title('Foo')
1482 title('Foo')
1477 assert_equal 'Foo - Redmine', html_title
1483 assert_equal 'Foo - Redmine', html_title
1478 end
1484 end
1479
1485
1480 def test_title_should_turn_arrays_into_links
1486 def test_title_should_turn_arrays_into_links
1481 assert_equal '<h2><a href="/foo">Foo</a></h2>', title(['Foo', '/foo'])
1487 assert_equal '<h2><a href="/foo">Foo</a></h2>', title(['Foo', '/foo'])
1482 assert_equal 'Foo - Redmine', html_title
1488 assert_equal 'Foo - Redmine', html_title
1483 end
1489 end
1484
1490
1485 def test_title_should_join_items
1491 def test_title_should_join_items
1486 assert_equal '<h2>Foo &#187; Bar</h2>', title('Foo', 'Bar')
1492 assert_equal '<h2>Foo &#187; Bar</h2>', title('Foo', 'Bar')
1487 assert_equal 'Bar - Foo - Redmine', html_title
1493 assert_equal 'Bar - Foo - Redmine', html_title
1488 end
1494 end
1489
1495
1490 def test_favicon_path
1496 def test_favicon_path
1491 assert_match %r{^/favicon\.ico}, favicon_path
1497 assert_match %r{^/favicon\.ico}, favicon_path
1492 end
1498 end
1493
1499
1494 def test_favicon_path_with_suburi
1500 def test_favicon_path_with_suburi
1495 Redmine::Utils.relative_url_root = '/foo'
1501 Redmine::Utils.relative_url_root = '/foo'
1496 assert_match %r{^/foo/favicon\.ico}, favicon_path
1502 assert_match %r{^/foo/favicon\.ico}, favicon_path
1497 ensure
1503 ensure
1498 Redmine::Utils.relative_url_root = ''
1504 Redmine::Utils.relative_url_root = ''
1499 end
1505 end
1500
1506
1501 def test_favicon_url
1507 def test_favicon_url
1502 assert_match %r{^http://test\.host/favicon\.ico}, favicon_url
1508 assert_match %r{^http://test\.host/favicon\.ico}, favicon_url
1503 end
1509 end
1504
1510
1505 def test_favicon_url_with_suburi
1511 def test_favicon_url_with_suburi
1506 Redmine::Utils.relative_url_root = '/foo'
1512 Redmine::Utils.relative_url_root = '/foo'
1507 assert_match %r{^http://test\.host/foo/favicon\.ico}, favicon_url
1513 assert_match %r{^http://test\.host/foo/favicon\.ico}, favicon_url
1508 ensure
1514 ensure
1509 Redmine::Utils.relative_url_root = ''
1515 Redmine::Utils.relative_url_root = ''
1510 end
1516 end
1511
1517
1512 def test_truncate_single_line
1518 def test_truncate_single_line
1513 str = "01234"
1519 str = "01234"
1514 result = truncate_single_line_raw("#{str}\n#{str}", 10)
1520 result = truncate_single_line_raw("#{str}\n#{str}", 10)
1515 assert_equal "01234 0...", result
1521 assert_equal "01234 0...", result
1516 assert !result.html_safe?
1522 assert !result.html_safe?
1517 result = truncate_single_line_raw("#{str}<&#>\n#{str}#{str}", 16)
1523 result = truncate_single_line_raw("#{str}<&#>\n#{str}#{str}", 16)
1518 assert_equal "01234<&#> 012...", result
1524 assert_equal "01234<&#> 012...", result
1519 assert !result.html_safe?
1525 assert !result.html_safe?
1520 end
1526 end
1521
1527
1522 def test_truncate_single_line_non_ascii
1528 def test_truncate_single_line_non_ascii
1523 ja = "\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e".force_encoding('UTF-8')
1529 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)
1530 result = truncate_single_line_raw("#{ja}\n#{ja}\n#{ja}", 10)
1525 assert_equal "#{ja} #{ja}...", result
1531 assert_equal "#{ja} #{ja}...", result
1526 assert !result.html_safe?
1532 assert !result.html_safe?
1527 end
1533 end
1528 end
1534 end
General Comments 0
You need to be logged in to leave comments. Login now