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