##// END OF EJS Templates
Merged #7563 from trunk (#7215)....
Etienne Massip -
r7444:605f1745ffc8
parent child
Show More
@@ -1,946 +1,951
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'forwardable'
18 require 'forwardable'
19 require 'cgi'
19 require 'cgi'
20
20
21 module ApplicationHelper
21 module ApplicationHelper
22 include Redmine::WikiFormatting::Macros::Definitions
22 include Redmine::WikiFormatting::Macros::Definitions
23 include Redmine::I18n
23 include Redmine::I18n
24 include GravatarHelper::PublicMethods
24 include GravatarHelper::PublicMethods
25
25
26 extend Forwardable
26 extend Forwardable
27 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
27 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
28
28
29 # Return true if user is authorized for controller/action, otherwise false
29 # Return true if user is authorized for controller/action, otherwise false
30 def authorize_for(controller, action)
30 def authorize_for(controller, action)
31 User.current.allowed_to?({:controller => controller, :action => action}, @project)
31 User.current.allowed_to?({:controller => controller, :action => action}, @project)
32 end
32 end
33
33
34 # Display a link if user is authorized
34 # Display a link if user is authorized
35 #
35 #
36 # @param [String] name Anchor text (passed to link_to)
36 # @param [String] name Anchor text (passed to link_to)
37 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
37 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
38 # @param [optional, Hash] html_options Options passed to link_to
38 # @param [optional, Hash] html_options Options passed to link_to
39 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
39 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
40 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
40 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
41 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
41 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
42 end
42 end
43
43
44 # Display a link to remote if user is authorized
44 # Display a link to remote if user is authorized
45 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
45 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
46 url = options[:url] || {}
46 url = options[:url] || {}
47 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
47 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
48 end
48 end
49
49
50 # Displays a link to user's account page if active
50 # Displays a link to user's account page if active
51 def link_to_user(user, options={})
51 def link_to_user(user, options={})
52 if user.is_a?(User)
52 if user.is_a?(User)
53 name = h(user.name(options[:format]))
53 name = h(user.name(options[:format]))
54 if user.active?
54 if user.active?
55 link_to name, :controller => 'users', :action => 'show', :id => user
55 link_to name, :controller => 'users', :action => 'show', :id => user
56 else
56 else
57 name
57 name
58 end
58 end
59 else
59 else
60 h(user.to_s)
60 h(user.to_s)
61 end
61 end
62 end
62 end
63
63
64 # Displays a link to +issue+ with its subject.
64 # Displays a link to +issue+ with its subject.
65 # Examples:
65 # Examples:
66 #
66 #
67 # link_to_issue(issue) # => Defect #6: This is the subject
67 # link_to_issue(issue) # => Defect #6: This is the subject
68 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
68 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
69 # link_to_issue(issue, :subject => false) # => Defect #6
69 # link_to_issue(issue, :subject => false) # => Defect #6
70 # link_to_issue(issue, :project => true) # => Foo - Defect #6
70 # link_to_issue(issue, :project => true) # => Foo - Defect #6
71 #
71 #
72 def link_to_issue(issue, options={})
72 def link_to_issue(issue, options={})
73 title = nil
73 title = nil
74 subject = nil
74 subject = nil
75 if options[:subject] == false
75 if options[:subject] == false
76 title = truncate(issue.subject, :length => 60)
76 title = truncate(issue.subject, :length => 60)
77 else
77 else
78 subject = issue.subject
78 subject = issue.subject
79 if options[:truncate]
79 if options[:truncate]
80 subject = truncate(subject, :length => options[:truncate])
80 subject = truncate(subject, :length => options[:truncate])
81 end
81 end
82 end
82 end
83 s = link_to "#{issue.tracker} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
83 s = link_to "#{issue.tracker} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
84 :class => issue.css_classes,
84 :class => issue.css_classes,
85 :title => title
85 :title => title
86 s << ": #{h subject}" if subject
86 s << ": #{h subject}" if subject
87 s = "#{h issue.project} - " + s if options[:project]
87 s = "#{h issue.project} - " + s if options[:project]
88 s
88 s
89 end
89 end
90
90
91 # Generates a link to an attachment.
91 # Generates a link to an attachment.
92 # Options:
92 # Options:
93 # * :text - Link text (default to attachment filename)
93 # * :text - Link text (default to attachment filename)
94 # * :download - Force download (default: false)
94 # * :download - Force download (default: false)
95 def link_to_attachment(attachment, options={})
95 def link_to_attachment(attachment, options={})
96 text = options.delete(:text) || attachment.filename
96 text = options.delete(:text) || attachment.filename
97 action = options.delete(:download) ? 'download' : 'show'
97 action = options.delete(:download) ? 'download' : 'show'
98
98
99 link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
99 link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
100 end
100 end
101
101
102 # Generates a link to a SCM revision
102 # Generates a link to a SCM revision
103 # Options:
103 # Options:
104 # * :text - Link text (default to the formatted revision)
104 # * :text - Link text (default to the formatted revision)
105 def link_to_revision(revision, project, options={})
105 def link_to_revision(revision, project, options={})
106 text = options.delete(:text) || format_revision(revision)
106 text = options.delete(:text) || format_revision(revision)
107 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
107 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
108
108
109 link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => rev},
109 link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => rev},
110 :title => l(:label_revision_id, format_revision(revision)))
110 :title => l(:label_revision_id, format_revision(revision)))
111 end
111 end
112
112
113 # Generates a link to a message
113 # Generates a link to a message
114 def link_to_message(message, options={}, html_options = nil)
114 def link_to_message(message, options={}, html_options = nil)
115 link_to(
115 link_to(
116 h(truncate(message.subject, :length => 60)),
116 h(truncate(message.subject, :length => 60)),
117 { :controller => 'messages', :action => 'show',
117 { :controller => 'messages', :action => 'show',
118 :board_id => message.board_id,
118 :board_id => message.board_id,
119 :id => message.root,
119 :id => message.root,
120 :r => (message.parent_id && message.id),
120 :r => (message.parent_id && message.id),
121 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
121 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
122 }.merge(options),
122 }.merge(options),
123 html_options
123 html_options
124 )
124 )
125 end
125 end
126
126
127 # Generates a link to a project if active
127 # Generates a link to a project if active
128 # Examples:
128 # Examples:
129 #
129 #
130 # link_to_project(project) # => link to the specified project overview
130 # link_to_project(project) # => link to the specified project overview
131 # link_to_project(project, :action=>'settings') # => link to project settings
131 # link_to_project(project, :action=>'settings') # => link to project settings
132 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
132 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
133 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
133 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
134 #
134 #
135 def link_to_project(project, options={}, html_options = nil)
135 def link_to_project(project, options={}, html_options = nil)
136 if project.active?
136 if project.active?
137 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
137 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
138 link_to(h(project), url, html_options)
138 link_to(h(project), url, html_options)
139 else
139 else
140 h(project)
140 h(project)
141 end
141 end
142 end
142 end
143
143
144 def toggle_link(name, id, options={})
144 def toggle_link(name, id, options={})
145 onclick = "Element.toggle('#{id}'); "
145 onclick = "Element.toggle('#{id}'); "
146 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
146 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
147 onclick << "return false;"
147 onclick << "return false;"
148 link_to(name, "#", :onclick => onclick)
148 link_to(name, "#", :onclick => onclick)
149 end
149 end
150
150
151 def image_to_function(name, function, html_options = {})
151 def image_to_function(name, function, html_options = {})
152 html_options.symbolize_keys!
152 html_options.symbolize_keys!
153 tag(:input, html_options.merge({
153 tag(:input, html_options.merge({
154 :type => "image", :src => image_path(name),
154 :type => "image", :src => image_path(name),
155 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
155 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
156 }))
156 }))
157 end
157 end
158
158
159 def prompt_to_remote(name, text, param, url, html_options = {})
159 def prompt_to_remote(name, text, param, url, html_options = {})
160 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
160 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
161 link_to name, {}, html_options
161 link_to name, {}, html_options
162 end
162 end
163
163
164 def format_activity_title(text)
164 def format_activity_title(text)
165 h(truncate_single_line(text, :length => 100))
165 h(truncate_single_line(text, :length => 100))
166 end
166 end
167
167
168 def format_activity_day(date)
168 def format_activity_day(date)
169 date == Date.today ? l(:label_today).titleize : format_date(date)
169 date == Date.today ? l(:label_today).titleize : format_date(date)
170 end
170 end
171
171
172 def format_activity_description(text)
172 def format_activity_description(text)
173 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
173 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
174 end
174 end
175
175
176 def format_version_name(version)
176 def format_version_name(version)
177 if version.project == @project
177 if version.project == @project
178 h(version)
178 h(version)
179 else
179 else
180 h("#{version.project} - #{version}")
180 h("#{version.project} - #{version}")
181 end
181 end
182 end
182 end
183
183
184 def due_date_distance_in_words(date)
184 def due_date_distance_in_words(date)
185 if date
185 if date
186 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
186 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
187 end
187 end
188 end
188 end
189
189
190 def render_page_hierarchy(pages, node=nil, options={})
190 def render_page_hierarchy(pages, node=nil, options={})
191 content = ''
191 content = ''
192 if pages[node]
192 if pages[node]
193 content << "<ul class=\"pages-hierarchy\">\n"
193 content << "<ul class=\"pages-hierarchy\">\n"
194 pages[node].each do |page|
194 pages[node].each do |page|
195 content << "<li>"
195 content << "<li>"
196 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
196 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
197 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
197 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
198 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
198 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
199 content << "</li>\n"
199 content << "</li>\n"
200 end
200 end
201 content << "</ul>\n"
201 content << "</ul>\n"
202 end
202 end
203 content
203 content
204 end
204 end
205
205
206 # Renders flash messages
206 # Renders flash messages
207 def render_flash_messages
207 def render_flash_messages
208 s = ''
208 s = ''
209 flash.each do |k,v|
209 flash.each do |k,v|
210 s << content_tag('div', v, :class => "flash #{k}")
210 s << content_tag('div', v, :class => "flash #{k}")
211 end
211 end
212 s
212 s
213 end
213 end
214
214
215 # Renders tabs and their content
215 # Renders tabs and their content
216 def render_tabs(tabs)
216 def render_tabs(tabs)
217 if tabs.any?
217 if tabs.any?
218 render :partial => 'common/tabs', :locals => {:tabs => tabs}
218 render :partial => 'common/tabs', :locals => {:tabs => tabs}
219 else
219 else
220 content_tag 'p', l(:label_no_data), :class => "nodata"
220 content_tag 'p', l(:label_no_data), :class => "nodata"
221 end
221 end
222 end
222 end
223
223
224 # Renders the project quick-jump box
224 # Renders the project quick-jump box
225 def render_project_jump_box
225 def render_project_jump_box
226 return unless User.current.logged?
226 return unless User.current.logged?
227 projects = User.current.memberships.collect(&:project).compact.uniq
227 projects = User.current.memberships.collect(&:project).compact.uniq
228 if projects.any?
228 if projects.any?
229 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
229 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
230 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
230 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
231 '<option value="" disabled="disabled">---</option>'
231 '<option value="" disabled="disabled">---</option>'
232 s << project_tree_options_for_select(projects, :selected => @project) do |p|
232 s << project_tree_options_for_select(projects, :selected => @project) do |p|
233 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
233 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
234 end
234 end
235 s << '</select>'
235 s << '</select>'
236 s
236 s
237 end
237 end
238 end
238 end
239
239
240 def project_tree_options_for_select(projects, options = {})
240 def project_tree_options_for_select(projects, options = {})
241 s = ''
241 s = ''
242 project_tree(projects) do |project, level|
242 project_tree(projects) do |project, level|
243 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
243 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
244 tag_options = {:value => project.id}
244 tag_options = {:value => project.id}
245 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
245 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
246 tag_options[:selected] = 'selected'
246 tag_options[:selected] = 'selected'
247 else
247 else
248 tag_options[:selected] = nil
248 tag_options[:selected] = nil
249 end
249 end
250 tag_options.merge!(yield(project)) if block_given?
250 tag_options.merge!(yield(project)) if block_given?
251 s << content_tag('option', name_prefix + h(project), tag_options)
251 s << content_tag('option', name_prefix + h(project), tag_options)
252 end
252 end
253 s
253 s
254 end
254 end
255
255
256 # Yields the given block for each project with its level in the tree
256 # Yields the given block for each project with its level in the tree
257 #
257 #
258 # Wrapper for Project#project_tree
258 # Wrapper for Project#project_tree
259 def project_tree(projects, &block)
259 def project_tree(projects, &block)
260 Project.project_tree(projects, &block)
260 Project.project_tree(projects, &block)
261 end
261 end
262
262
263 def project_nested_ul(projects, &block)
263 def project_nested_ul(projects, &block)
264 s = ''
264 s = ''
265 if projects.any?
265 if projects.any?
266 ancestors = []
266 ancestors = []
267 projects.sort_by(&:lft).each do |project|
267 projects.sort_by(&:lft).each do |project|
268 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
268 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
269 s << "<ul>\n"
269 s << "<ul>\n"
270 else
270 else
271 ancestors.pop
271 ancestors.pop
272 s << "</li>"
272 s << "</li>"
273 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
273 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
274 ancestors.pop
274 ancestors.pop
275 s << "</ul></li>\n"
275 s << "</ul></li>\n"
276 end
276 end
277 end
277 end
278 s << "<li>"
278 s << "<li>"
279 s << yield(project).to_s
279 s << yield(project).to_s
280 ancestors << project
280 ancestors << project
281 end
281 end
282 s << ("</li></ul>\n" * ancestors.size)
282 s << ("</li></ul>\n" * ancestors.size)
283 end
283 end
284 s
284 s
285 end
285 end
286
286
287 def principals_check_box_tags(name, principals)
287 def principals_check_box_tags(name, principals)
288 s = ''
288 s = ''
289 principals.sort.each do |principal|
289 principals.sort.each do |principal|
290 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
290 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
291 end
291 end
292 s
292 s
293 end
293 end
294
294
295 # Truncates and returns the string as a single line
295 # Truncates and returns the string as a single line
296 def truncate_single_line(string, *args)
296 def truncate_single_line(string, *args)
297 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
297 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
298 end
298 end
299
299
300 # Truncates at line break after 250 characters or options[:length]
300 # Truncates at line break after 250 characters or options[:length]
301 def truncate_lines(string, options={})
301 def truncate_lines(string, options={})
302 length = options[:length] || 250
302 length = options[:length] || 250
303 if string.to_s =~ /\A(.{#{length}}.*?)$/m
303 if string.to_s =~ /\A(.{#{length}}.*?)$/m
304 "#{$1}..."
304 "#{$1}..."
305 else
305 else
306 string
306 string
307 end
307 end
308 end
308 end
309
309
310 def html_hours(text)
310 def html_hours(text)
311 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
311 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
312 end
312 end
313
313
314 def authoring(created, author, options={})
314 def authoring(created, author, options={})
315 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created))
315 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created))
316 end
316 end
317
317
318 def time_tag(time)
318 def time_tag(time)
319 text = distance_of_time_in_words(Time.now, time)
319 text = distance_of_time_in_words(Time.now, time)
320 if @project
320 if @project
321 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
321 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
322 else
322 else
323 content_tag('acronym', text, :title => format_time(time))
323 content_tag('acronym', text, :title => format_time(time))
324 end
324 end
325 end
325 end
326
326
327 def syntax_highlight(name, content)
327 def syntax_highlight(name, content)
328 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
328 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
329 end
329 end
330
330
331 def to_path_param(path)
331 def to_path_param(path)
332 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
332 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
333 end
333 end
334
334
335 def pagination_links_full(paginator, count=nil, options={})
335 def pagination_links_full(paginator, count=nil, options={})
336 page_param = options.delete(:page_param) || :page
336 page_param = options.delete(:page_param) || :page
337 per_page_links = options.delete(:per_page_links)
337 per_page_links = options.delete(:per_page_links)
338 url_param = params.dup
338 url_param = params.dup
339
339
340 html = ''
340 html = ''
341 if paginator.current.previous
341 if paginator.current.previous
342 html << link_to_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
342 html << link_to_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
343 end
343 end
344
344
345 html << (pagination_links_each(paginator, options) do |n|
345 html << (pagination_links_each(paginator, options) do |n|
346 link_to_content_update(n.to_s, url_param.merge(page_param => n))
346 link_to_content_update(n.to_s, url_param.merge(page_param => n))
347 end || '')
347 end || '')
348
348
349 if paginator.current.next
349 if paginator.current.next
350 html << ' ' + link_to_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next))
350 html << ' ' + link_to_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next))
351 end
351 end
352
352
353 unless count.nil?
353 unless count.nil?
354 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
354 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
355 if per_page_links != false && links = per_page_links(paginator.items_per_page)
355 if per_page_links != false && links = per_page_links(paginator.items_per_page)
356 html << " | #{links}"
356 html << " | #{links}"
357 end
357 end
358 end
358 end
359
359
360 html
360 html
361 end
361 end
362
362
363 def per_page_links(selected=nil)
363 def per_page_links(selected=nil)
364 links = Setting.per_page_options_array.collect do |n|
364 links = Setting.per_page_options_array.collect do |n|
365 n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
365 n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
366 end
366 end
367 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
367 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
368 end
368 end
369
369
370 def reorder_links(name, url)
370 def reorder_links(name, url)
371 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), url.merge({"#{name}[move_to]" => 'highest'}), :method => :post, :title => l(:label_sort_highest)) +
371 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), url.merge({"#{name}[move_to]" => 'highest'}), :method => :post, :title => l(:label_sort_highest)) +
372 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), url.merge({"#{name}[move_to]" => 'higher'}), :method => :post, :title => l(:label_sort_higher)) +
372 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), url.merge({"#{name}[move_to]" => 'higher'}), :method => :post, :title => l(:label_sort_higher)) +
373 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), url.merge({"#{name}[move_to]" => 'lower'}), :method => :post, :title => l(:label_sort_lower)) +
373 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), url.merge({"#{name}[move_to]" => 'lower'}), :method => :post, :title => l(:label_sort_lower)) +
374 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), url.merge({"#{name}[move_to]" => 'lowest'}), :method => :post, :title => l(:label_sort_lowest))
374 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), url.merge({"#{name}[move_to]" => 'lowest'}), :method => :post, :title => l(:label_sort_lowest))
375 end
375 end
376
376
377 def breadcrumb(*args)
377 def breadcrumb(*args)
378 elements = args.flatten
378 elements = args.flatten
379 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
379 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
380 end
380 end
381
381
382 def other_formats_links(&block)
382 def other_formats_links(&block)
383 concat('<p class="other-formats">' + l(:label_export_to))
383 concat('<p class="other-formats">' + l(:label_export_to))
384 yield Redmine::Views::OtherFormatsBuilder.new(self)
384 yield Redmine::Views::OtherFormatsBuilder.new(self)
385 concat('</p>')
385 concat('</p>')
386 end
386 end
387
387
388 def page_header_title
388 def page_header_title
389 if @project.nil? || @project.new_record?
389 if @project.nil? || @project.new_record?
390 h(Setting.app_title)
390 h(Setting.app_title)
391 else
391 else
392 b = []
392 b = []
393 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
393 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
394 if ancestors.any?
394 if ancestors.any?
395 root = ancestors.shift
395 root = ancestors.shift
396 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
396 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
397 if ancestors.size > 2
397 if ancestors.size > 2
398 b << '&#8230;'
398 b << '&#8230;'
399 ancestors = ancestors[-2, 2]
399 ancestors = ancestors[-2, 2]
400 end
400 end
401 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
401 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
402 end
402 end
403 b << h(@project)
403 b << h(@project)
404 b.join(' &#187; ')
404 b.join(' &#187; ')
405 end
405 end
406 end
406 end
407
407
408 def html_title(*args)
408 def html_title(*args)
409 if args.empty?
409 if args.empty?
410 title = []
410 title = []
411 title << @project.name if @project
411 title << @project.name if @project
412 title += @html_title if @html_title
412 title += @html_title if @html_title
413 title << Setting.app_title
413 title << Setting.app_title
414 title.select {|t| !t.blank? }.join(' - ')
414 title.select {|t| !t.blank? }.join(' - ')
415 else
415 else
416 @html_title ||= []
416 @html_title ||= []
417 @html_title += args
417 @html_title += args
418 end
418 end
419 end
419 end
420
420
421 # Returns the theme, controller name, and action as css classes for the
421 # Returns the theme, controller name, and action as css classes for the
422 # HTML body.
422 # HTML body.
423 def body_css_classes
423 def body_css_classes
424 css = []
424 css = []
425 if theme = Redmine::Themes.theme(Setting.ui_theme)
425 if theme = Redmine::Themes.theme(Setting.ui_theme)
426 css << 'theme-' + theme.name
426 css << 'theme-' + theme.name
427 end
427 end
428
428
429 css << 'controller-' + params[:controller]
429 css << 'controller-' + params[:controller]
430 css << 'action-' + params[:action]
430 css << 'action-' + params[:action]
431 css.join(' ')
431 css.join(' ')
432 end
432 end
433
433
434 def accesskey(s)
434 def accesskey(s)
435 Redmine::AccessKeys.key_for s
435 Redmine::AccessKeys.key_for s
436 end
436 end
437
437
438 # Formats text according to system settings.
438 # Formats text according to system settings.
439 # 2 ways to call this method:
439 # 2 ways to call this method:
440 # * with a String: textilizable(text, options)
440 # * with a String: textilizable(text, options)
441 # * with an object and one of its attribute: textilizable(issue, :description, options)
441 # * with an object and one of its attribute: textilizable(issue, :description, options)
442 def textilizable(*args)
442 def textilizable(*args)
443 options = args.last.is_a?(Hash) ? args.pop : {}
443 options = args.last.is_a?(Hash) ? args.pop : {}
444 case args.size
444 case args.size
445 when 1
445 when 1
446 obj = options[:object]
446 obj = options[:object]
447 text = args.shift
447 text = args.shift
448 when 2
448 when 2
449 obj = args.shift
449 obj = args.shift
450 attr = args.shift
450 attr = args.shift
451 text = obj.send(attr).to_s
451 text = obj.send(attr).to_s
452 else
452 else
453 raise ArgumentError, 'invalid arguments to textilizable'
453 raise ArgumentError, 'invalid arguments to textilizable'
454 end
454 end
455 return '' if text.blank?
455 return '' if text.blank?
456 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
456 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
457 only_path = options.delete(:only_path) == false ? false : true
457 only_path = options.delete(:only_path) == false ? false : true
458
458
459 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }
459 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }
460
460
461 @parsed_headings = []
461 @parsed_headings = []
462 text = parse_non_pre_blocks(text) do |text|
462 text = parse_non_pre_blocks(text) do |text|
463 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_headings].each do |method_name|
463 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_headings].each do |method_name|
464 send method_name, text, project, obj, attr, only_path, options
464 send method_name, text, project, obj, attr, only_path, options
465 end
465 end
466 end
466 end
467
467
468 if @parsed_headings.any?
468 if @parsed_headings.any?
469 replace_toc(text, @parsed_headings)
469 replace_toc(text, @parsed_headings)
470 end
470 end
471
471
472 text
472 text
473 end
473 end
474
474
475 def parse_non_pre_blocks(text)
475 def parse_non_pre_blocks(text)
476 s = StringScanner.new(text)
476 s = StringScanner.new(text)
477 tags = []
477 tags = []
478 parsed = ''
478 parsed = ''
479 while !s.eos?
479 while !s.eos?
480 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
480 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
481 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
481 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
482 if tags.empty?
482 if tags.empty?
483 yield text
483 yield text
484 end
484 end
485 parsed << text
485 parsed << text
486 if tag
486 if tag
487 if closing
487 if closing
488 if tags.last == tag.downcase
488 if tags.last == tag.downcase
489 tags.pop
489 tags.pop
490 end
490 end
491 else
491 else
492 tags << tag.downcase
492 tags << tag.downcase
493 end
493 end
494 parsed << full_tag
494 parsed << full_tag
495 end
495 end
496 end
496 end
497 # Close any non closing tags
497 # Close any non closing tags
498 while tag = tags.pop
498 while tag = tags.pop
499 parsed << "</#{tag}>"
499 parsed << "</#{tag}>"
500 end
500 end
501 parsed
501 parsed
502 end
502 end
503
503
504 def parse_inline_attachments(text, project, obj, attr, only_path, options)
504 def parse_inline_attachments(text, project, obj, attr, only_path, options)
505 # when using an image link, try to use an attachment, if possible
505 # when using an image link, try to use an attachment, if possible
506 if options[:attachments] || (obj && obj.respond_to?(:attachments))
506 if options[:attachments] || (obj && obj.respond_to?(:attachments))
507 attachments = nil
507 attachments = nil
508 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
508 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
509 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
509 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
510 attachments ||= (options[:attachments] || obj.attachments).sort_by(&:created_on).reverse
510 attachments ||= (options[:attachments] || obj.attachments).sort_by(&:created_on).reverse
511 # search for the picture in attachments
511 # search for the picture in attachments
512 if found = attachments.detect { |att| att.filename.downcase == filename }
512 if found = attachments.detect { |att| att.filename.downcase == filename }
513 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
513 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
514 desc = found.description.to_s.gsub('"', '')
514 desc = found.description.to_s.gsub('"', '')
515 if !desc.blank? && alttext.blank?
515 if !desc.blank? && alttext.blank?
516 alt = " title=\"#{desc}\" alt=\"#{desc}\""
516 alt = " title=\"#{desc}\" alt=\"#{desc}\""
517 end
517 end
518 "src=\"#{image_url}\"#{alt}"
518 "src=\"#{image_url}\"#{alt}"
519 else
519 else
520 m
520 m
521 end
521 end
522 end
522 end
523 end
523 end
524 end
524 end
525
525
526 # Wiki links
526 # Wiki links
527 #
527 #
528 # Examples:
528 # Examples:
529 # [[mypage]]
529 # [[mypage]]
530 # [[mypage|mytext]]
530 # [[mypage|mytext]]
531 # wiki links can refer other project wikis, using project name or identifier:
531 # wiki links can refer other project wikis, using project name or identifier:
532 # [[project:]] -> wiki starting page
532 # [[project:]] -> wiki starting page
533 # [[project:|mytext]]
533 # [[project:|mytext]]
534 # [[project:mypage]]
534 # [[project:mypage]]
535 # [[project:mypage|mytext]]
535 # [[project:mypage|mytext]]
536 def parse_wiki_links(text, project, obj, attr, only_path, options)
536 def parse_wiki_links(text, project, obj, attr, only_path, options)
537 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
537 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
538 link_project = project
538 link_project = project
539 esc, all, page, title = $1, $2, $3, $5
539 esc, all, page, title = $1, $2, $3, $5
540 if esc.nil?
540 if esc.nil?
541 if page =~ /^([^\:]+)\:(.*)$/
541 if page =~ /^([^\:]+)\:(.*)$/
542 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
542 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
543 page = $2
543 page = $2
544 title ||= $1 if page.blank?
544 title ||= $1 if page.blank?
545 end
545 end
546
546
547 if link_project && link_project.wiki
547 if link_project && link_project.wiki
548 # extract anchor
548 # extract anchor
549 anchor = nil
549 anchor = nil
550 if page =~ /^(.+?)\#(.+)$/
550 if page =~ /^(.+?)\#(.+)$/
551 page, anchor = $1, $2
551 page, anchor = $1, $2
552 end
552 end
553 anchor = sanitize_anchor_name(anchor) if anchor.present?
553 # check if page exists
554 # check if page exists
554 wiki_page = link_project.wiki.find_page(page)
555 wiki_page = link_project.wiki.find_page(page)
555 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
556 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
556 "##{anchor}"
557 "##{anchor}"
557 else
558 else
558 case options[:wiki_links]
559 case options[:wiki_links]
559 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
560 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
560 when :anchor; "##{title}" # used for single-file wiki export
561 when :anchor; "##{title}" # used for single-file wiki export
561 else
562 else
562 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
563 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
563 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, :id => wiki_page_id, :anchor => anchor)
564 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, :id => wiki_page_id, :anchor => anchor)
564 end
565 end
565 end
566 end
566 link_to((title || page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
567 link_to((title || page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
567 else
568 else
568 # project or wiki doesn't exist
569 # project or wiki doesn't exist
569 all
570 all
570 end
571 end
571 else
572 else
572 all
573 all
573 end
574 end
574 end
575 end
575 end
576 end
576
577
577 # Redmine links
578 # Redmine links
578 #
579 #
579 # Examples:
580 # Examples:
580 # Issues:
581 # Issues:
581 # #52 -> Link to issue #52
582 # #52 -> Link to issue #52
582 # Changesets:
583 # Changesets:
583 # r52 -> Link to revision 52
584 # r52 -> Link to revision 52
584 # commit:a85130f -> Link to scmid starting with a85130f
585 # commit:a85130f -> Link to scmid starting with a85130f
585 # Documents:
586 # Documents:
586 # document#17 -> Link to document with id 17
587 # document#17 -> Link to document with id 17
587 # document:Greetings -> Link to the document with title "Greetings"
588 # document:Greetings -> Link to the document with title "Greetings"
588 # document:"Some document" -> Link to the document with title "Some document"
589 # document:"Some document" -> Link to the document with title "Some document"
589 # Versions:
590 # Versions:
590 # version#3 -> Link to version with id 3
591 # version#3 -> Link to version with id 3
591 # version:1.0.0 -> Link to version named "1.0.0"
592 # version:1.0.0 -> Link to version named "1.0.0"
592 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
593 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
593 # Attachments:
594 # Attachments:
594 # attachment:file.zip -> Link to the attachment of the current object named file.zip
595 # attachment:file.zip -> Link to the attachment of the current object named file.zip
595 # Source files:
596 # Source files:
596 # source:some/file -> Link to the file located at /some/file in the project's repository
597 # source:some/file -> Link to the file located at /some/file in the project's repository
597 # source:some/file@52 -> Link to the file's revision 52
598 # source:some/file@52 -> Link to the file's revision 52
598 # source:some/file#L120 -> Link to line 120 of the file
599 # source:some/file#L120 -> Link to line 120 of the file
599 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
600 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
600 # export:some/file -> Force the download of the file
601 # export:some/file -> Force the download of the file
601 # Forum messages:
602 # Forum messages:
602 # message#1218 -> Link to message with id 1218
603 # message#1218 -> Link to message with id 1218
603 #
604 #
604 # Links can refer other objects from other projects, using project identifier:
605 # Links can refer other objects from other projects, using project identifier:
605 # identifier:r52
606 # identifier:r52
606 # identifier:document:"Some document"
607 # identifier:document:"Some document"
607 # identifier:version:1.0.0
608 # identifier:version:1.0.0
608 # identifier:source:some/file
609 # identifier:source:some/file
609 def parse_redmine_links(text, project, obj, attr, only_path, options)
610 def parse_redmine_links(text, project, obj, attr, only_path, options)
610 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-]+):)?(attachment|document|version|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m|
611 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-]+):)?(attachment|document|version|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m|
611 leading, esc, project_prefix, project_identifier, prefix, sep, identifier = $1, $2, $3, $4, $5, $7 || $9, $8 || $10
612 leading, esc, project_prefix, project_identifier, prefix, sep, identifier = $1, $2, $3, $4, $5, $7 || $9, $8 || $10
612 link = nil
613 link = nil
613 if project_identifier
614 if project_identifier
614 project = Project.visible.find_by_identifier(project_identifier)
615 project = Project.visible.find_by_identifier(project_identifier)
615 end
616 end
616 if esc.nil?
617 if esc.nil?
617 if prefix.nil? && sep == 'r'
618 if prefix.nil? && sep == 'r'
618 # project.changesets.visible raises an SQL error because of a double join on repositories
619 # project.changesets.visible raises an SQL error because of a double join on repositories
619 if project && project.repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(project.repository.id, identifier))
620 if project && project.repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(project.repository.id, identifier))
620 link = link_to("#{project_prefix}r#{identifier}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
621 link = link_to("#{project_prefix}r#{identifier}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
621 :class => 'changeset',
622 :class => 'changeset',
622 :title => truncate_single_line(changeset.comments, :length => 100))
623 :title => truncate_single_line(changeset.comments, :length => 100))
623 end
624 end
624 elsif sep == '#'
625 elsif sep == '#'
625 oid = identifier.to_i
626 oid = identifier.to_i
626 case prefix
627 case prefix
627 when nil
628 when nil
628 if issue = Issue.visible.find_by_id(oid, :include => :status)
629 if issue = Issue.visible.find_by_id(oid, :include => :status)
629 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
630 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
630 :class => issue.css_classes,
631 :class => issue.css_classes,
631 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
632 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
632 end
633 end
633 when 'document'
634 when 'document'
634 if document = Document.visible.find_by_id(oid)
635 if document = Document.visible.find_by_id(oid)
635 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
636 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
636 :class => 'document'
637 :class => 'document'
637 end
638 end
638 when 'version'
639 when 'version'
639 if version = Version.visible.find_by_id(oid)
640 if version = Version.visible.find_by_id(oid)
640 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
641 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
641 :class => 'version'
642 :class => 'version'
642 end
643 end
643 when 'message'
644 when 'message'
644 if message = Message.visible.find_by_id(oid, :include => :parent)
645 if message = Message.visible.find_by_id(oid, :include => :parent)
645 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
646 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
646 end
647 end
647 when 'project'
648 when 'project'
648 if p = Project.visible.find_by_id(oid)
649 if p = Project.visible.find_by_id(oid)
649 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
650 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
650 end
651 end
651 end
652 end
652 elsif sep == ':'
653 elsif sep == ':'
653 # removes the double quotes if any
654 # removes the double quotes if any
654 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
655 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
655 case prefix
656 case prefix
656 when 'document'
657 when 'document'
657 if project && document = project.documents.visible.find_by_title(name)
658 if project && document = project.documents.visible.find_by_title(name)
658 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
659 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
659 :class => 'document'
660 :class => 'document'
660 end
661 end
661 when 'version'
662 when 'version'
662 if project && version = project.versions.visible.find_by_name(name)
663 if project && version = project.versions.visible.find_by_name(name)
663 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
664 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
664 :class => 'version'
665 :class => 'version'
665 end
666 end
666 when 'commit'
667 when 'commit'
667 if project && project.repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", project.repository.id, "#{name}%"]))
668 if project && project.repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", project.repository.id, "#{name}%"]))
668 link = link_to h("#{project_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.identifier},
669 link = link_to h("#{project_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.identifier},
669 :class => 'changeset',
670 :class => 'changeset',
670 :title => truncate_single_line(changeset.comments, :length => 100)
671 :title => truncate_single_line(changeset.comments, :length => 100)
671 end
672 end
672 when 'source', 'export'
673 when 'source', 'export'
673 if project && project.repository && User.current.allowed_to?(:browse_repository, project)
674 if project && project.repository && User.current.allowed_to?(:browse_repository, project)
674 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
675 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
675 path, rev, anchor = $1, $3, $5
676 path, rev, anchor = $1, $3, $5
676 link = link_to h("#{project_prefix}#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
677 link = link_to h("#{project_prefix}#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
677 :path => to_path_param(path),
678 :path => to_path_param(path),
678 :rev => rev,
679 :rev => rev,
679 :anchor => anchor,
680 :anchor => anchor,
680 :format => (prefix == 'export' ? 'raw' : nil)},
681 :format => (prefix == 'export' ? 'raw' : nil)},
681 :class => (prefix == 'export' ? 'source download' : 'source')
682 :class => (prefix == 'export' ? 'source download' : 'source')
682 end
683 end
683 when 'attachment'
684 when 'attachment'
684 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
685 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
685 if attachments && attachment = attachments.detect {|a| a.filename == name }
686 if attachments && attachment = attachments.detect {|a| a.filename == name }
686 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
687 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
687 :class => 'attachment'
688 :class => 'attachment'
688 end
689 end
689 when 'project'
690 when 'project'
690 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
691 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
691 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
692 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
692 end
693 end
693 end
694 end
694 end
695 end
695 end
696 end
696 leading + (link || "#{project_prefix}#{prefix}#{sep}#{identifier}")
697 leading + (link || "#{project_prefix}#{prefix}#{sep}#{identifier}")
697 end
698 end
698 end
699 end
699
700
700 HEADING_RE = /<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>/i unless const_defined?(:HEADING_RE)
701 HEADING_RE = /<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>/i unless const_defined?(:HEADING_RE)
701
702
702 # Headings and TOC
703 # Headings and TOC
703 # Adds ids and links to headings unless options[:headings] is set to false
704 # Adds ids and links to headings unless options[:headings] is set to false
704 def parse_headings(text, project, obj, attr, only_path, options)
705 def parse_headings(text, project, obj, attr, only_path, options)
705 return if options[:headings] == false
706 return if options[:headings] == false
706
707
707 text.gsub!(HEADING_RE) do
708 text.gsub!(HEADING_RE) do
708 level, attrs, content = $1.to_i, $2, $3
709 level, attrs, content = $1.to_i, $2, $3
709 item = strip_tags(content).strip
710 item = strip_tags(content).strip
710 anchor = item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
711 anchor = sanitize_anchor_name(item)
711 @parsed_headings << [level, anchor, item]
712 @parsed_headings << [level, anchor, item]
712 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
713 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
713 end
714 end
714 end
715 end
715
716
716 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
717 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
717
718
718 # Renders the TOC with given headings
719 # Renders the TOC with given headings
719 def replace_toc(text, headings)
720 def replace_toc(text, headings)
720 text.gsub!(TOC_RE) do
721 text.gsub!(TOC_RE) do
721 if headings.empty?
722 if headings.empty?
722 ''
723 ''
723 else
724 else
724 div_class = 'toc'
725 div_class = 'toc'
725 div_class << ' right' if $1 == '>'
726 div_class << ' right' if $1 == '>'
726 div_class << ' left' if $1 == '<'
727 div_class << ' left' if $1 == '<'
727 out = "<ul class=\"#{div_class}\"><li>"
728 out = "<ul class=\"#{div_class}\"><li>"
728 root = headings.map(&:first).min
729 root = headings.map(&:first).min
729 current = root
730 current = root
730 started = false
731 started = false
731 headings.each do |level, anchor, item|
732 headings.each do |level, anchor, item|
732 if level > current
733 if level > current
733 out << '<ul><li>' * (level - current)
734 out << '<ul><li>' * (level - current)
734 elsif level < current
735 elsif level < current
735 out << "</li></ul>\n" * (current - level) + "</li><li>"
736 out << "</li></ul>\n" * (current - level) + "</li><li>"
736 elsif started
737 elsif started
737 out << '</li><li>'
738 out << '</li><li>'
738 end
739 end
739 out << "<a href=\"##{anchor}\">#{item}</a>"
740 out << "<a href=\"##{anchor}\">#{item}</a>"
740 current = level
741 current = level
741 started = true
742 started = true
742 end
743 end
743 out << '</li></ul>' * (current - root)
744 out << '</li></ul>' * (current - root)
744 out << '</li></ul>'
745 out << '</li></ul>'
745 end
746 end
746 end
747 end
747 end
748 end
748
749
749 # Same as Rails' simple_format helper without using paragraphs
750 # Same as Rails' simple_format helper without using paragraphs
750 def simple_format_without_paragraph(text)
751 def simple_format_without_paragraph(text)
751 text.to_s.
752 text.to_s.
752 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
753 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
753 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
754 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
754 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
755 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
755 end
756 end
756
757
757 def lang_options_for_select(blank=true)
758 def lang_options_for_select(blank=true)
758 (blank ? [["(auto)", ""]] : []) +
759 (blank ? [["(auto)", ""]] : []) +
759 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
760 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
760 end
761 end
761
762
762 def label_tag_for(name, option_tags = nil, options = {})
763 def label_tag_for(name, option_tags = nil, options = {})
763 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
764 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
764 content_tag("label", label_text)
765 content_tag("label", label_text)
765 end
766 end
766
767
767 def labelled_tabular_form_for(name, object, options, &proc)
768 def labelled_tabular_form_for(name, object, options, &proc)
768 options[:html] ||= {}
769 options[:html] ||= {}
769 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
770 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
770 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
771 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
771 end
772 end
772
773
773 def back_url_hidden_field_tag
774 def back_url_hidden_field_tag
774 back_url = params[:back_url] || request.env['HTTP_REFERER']
775 back_url = params[:back_url] || request.env['HTTP_REFERER']
775 back_url = CGI.unescape(back_url.to_s)
776 back_url = CGI.unescape(back_url.to_s)
776 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
777 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
777 end
778 end
778
779
779 def check_all_links(form_name)
780 def check_all_links(form_name)
780 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
781 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
781 " | " +
782 " | " +
782 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
783 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
783 end
784 end
784
785
785 def progress_bar(pcts, options={})
786 def progress_bar(pcts, options={})
786 pcts = [pcts, pcts] unless pcts.is_a?(Array)
787 pcts = [pcts, pcts] unless pcts.is_a?(Array)
787 pcts = pcts.collect(&:round)
788 pcts = pcts.collect(&:round)
788 pcts[1] = pcts[1] - pcts[0]
789 pcts[1] = pcts[1] - pcts[0]
789 pcts << (100 - pcts[1] - pcts[0])
790 pcts << (100 - pcts[1] - pcts[0])
790 width = options[:width] || '100px;'
791 width = options[:width] || '100px;'
791 legend = options[:legend] || ''
792 legend = options[:legend] || ''
792 content_tag('table',
793 content_tag('table',
793 content_tag('tr',
794 content_tag('tr',
794 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : '') +
795 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : '') +
795 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : '') +
796 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : '') +
796 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : '')
797 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : '')
797 ), :class => 'progress', :style => "width: #{width};") +
798 ), :class => 'progress', :style => "width: #{width};") +
798 content_tag('p', legend, :class => 'pourcent')
799 content_tag('p', legend, :class => 'pourcent')
799 end
800 end
800
801
801 def checked_image(checked=true)
802 def checked_image(checked=true)
802 if checked
803 if checked
803 image_tag 'toggle_check.png'
804 image_tag 'toggle_check.png'
804 end
805 end
805 end
806 end
806
807
807 def context_menu(url)
808 def context_menu(url)
808 unless @context_menu_included
809 unless @context_menu_included
809 content_for :header_tags do
810 content_for :header_tags do
810 javascript_include_tag('context_menu') +
811 javascript_include_tag('context_menu') +
811 stylesheet_link_tag('context_menu')
812 stylesheet_link_tag('context_menu')
812 end
813 end
813 if l(:direction) == 'rtl'
814 if l(:direction) == 'rtl'
814 content_for :header_tags do
815 content_for :header_tags do
815 stylesheet_link_tag('context_menu_rtl')
816 stylesheet_link_tag('context_menu_rtl')
816 end
817 end
817 end
818 end
818 @context_menu_included = true
819 @context_menu_included = true
819 end
820 end
820 javascript_tag "new ContextMenu('#{ url_for(url) }')"
821 javascript_tag "new ContextMenu('#{ url_for(url) }')"
821 end
822 end
822
823
823 def context_menu_link(name, url, options={})
824 def context_menu_link(name, url, options={})
824 options[:class] ||= ''
825 options[:class] ||= ''
825 if options.delete(:selected)
826 if options.delete(:selected)
826 options[:class] << ' icon-checked disabled'
827 options[:class] << ' icon-checked disabled'
827 options[:disabled] = true
828 options[:disabled] = true
828 end
829 end
829 if options.delete(:disabled)
830 if options.delete(:disabled)
830 options.delete(:method)
831 options.delete(:method)
831 options.delete(:confirm)
832 options.delete(:confirm)
832 options.delete(:onclick)
833 options.delete(:onclick)
833 options[:class] << ' disabled'
834 options[:class] << ' disabled'
834 url = '#'
835 url = '#'
835 end
836 end
836 link_to name, url, options
837 link_to name, url, options
837 end
838 end
838
839
839 def calendar_for(field_id)
840 def calendar_for(field_id)
840 include_calendar_headers_tags
841 include_calendar_headers_tags
841 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
842 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
842 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
843 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
843 end
844 end
844
845
845 def include_calendar_headers_tags
846 def include_calendar_headers_tags
846 unless @calendar_headers_tags_included
847 unless @calendar_headers_tags_included
847 @calendar_headers_tags_included = true
848 @calendar_headers_tags_included = true
848 content_for :header_tags do
849 content_for :header_tags do
849 start_of_week = case Setting.start_of_week.to_i
850 start_of_week = case Setting.start_of_week.to_i
850 when 1
851 when 1
851 'Calendar._FD = 1;' # Monday
852 'Calendar._FD = 1;' # Monday
852 when 7
853 when 7
853 'Calendar._FD = 0;' # Sunday
854 'Calendar._FD = 0;' # Sunday
854 when 6
855 when 6
855 'Calendar._FD = 6;' # Saturday
856 'Calendar._FD = 6;' # Saturday
856 else
857 else
857 '' # use language
858 '' # use language
858 end
859 end
859
860
860 javascript_include_tag('calendar/calendar') +
861 javascript_include_tag('calendar/calendar') +
861 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
862 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
862 javascript_tag(start_of_week) +
863 javascript_tag(start_of_week) +
863 javascript_include_tag('calendar/calendar-setup') +
864 javascript_include_tag('calendar/calendar-setup') +
864 stylesheet_link_tag('calendar')
865 stylesheet_link_tag('calendar')
865 end
866 end
866 end
867 end
867 end
868 end
868
869
869 def content_for(name, content = nil, &block)
870 def content_for(name, content = nil, &block)
870 @has_content ||= {}
871 @has_content ||= {}
871 @has_content[name] = true
872 @has_content[name] = true
872 super(name, content, &block)
873 super(name, content, &block)
873 end
874 end
874
875
875 def has_content?(name)
876 def has_content?(name)
876 (@has_content && @has_content[name]) || false
877 (@has_content && @has_content[name]) || false
877 end
878 end
878
879
879 # Returns the avatar image tag for the given +user+ if avatars are enabled
880 # Returns the avatar image tag for the given +user+ if avatars are enabled
880 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
881 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
881 def avatar(user, options = { })
882 def avatar(user, options = { })
882 if Setting.gravatar_enabled?
883 if Setting.gravatar_enabled?
883 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
884 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
884 email = nil
885 email = nil
885 if user.respond_to?(:mail)
886 if user.respond_to?(:mail)
886 email = user.mail
887 email = user.mail
887 elsif user.to_s =~ %r{<(.+?)>}
888 elsif user.to_s =~ %r{<(.+?)>}
888 email = $1
889 email = $1
889 end
890 end
890 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
891 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
891 else
892 else
892 ''
893 ''
893 end
894 end
894 end
895 end
895
896
897 def sanitize_anchor_name(anchor)
898 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
899 end
900
896 # Returns the javascript tags that are included in the html layout head
901 # Returns the javascript tags that are included in the html layout head
897 def javascript_heads
902 def javascript_heads
898 tags = javascript_include_tag(:defaults)
903 tags = javascript_include_tag(:defaults)
899 unless User.current.pref.warn_on_leaving_unsaved == '0'
904 unless User.current.pref.warn_on_leaving_unsaved == '0'
900 tags << "\n" + javascript_tag("Event.observe(window, 'load', function(){ new WarnLeavingUnsaved('#{escape_javascript( l(:text_warn_on_leaving_unsaved) )}'); });")
905 tags << "\n" + javascript_tag("Event.observe(window, 'load', function(){ new WarnLeavingUnsaved('#{escape_javascript( l(:text_warn_on_leaving_unsaved) )}'); });")
901 end
906 end
902 tags
907 tags
903 end
908 end
904
909
905 def favicon
910 def favicon
906 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />"
911 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />"
907 end
912 end
908
913
909 def robot_exclusion_tag
914 def robot_exclusion_tag
910 '<meta name="robots" content="noindex,follow,noarchive" />'
915 '<meta name="robots" content="noindex,follow,noarchive" />'
911 end
916 end
912
917
913 # Returns true if arg is expected in the API response
918 # Returns true if arg is expected in the API response
914 def include_in_api_response?(arg)
919 def include_in_api_response?(arg)
915 unless @included_in_api_response
920 unless @included_in_api_response
916 param = params[:include]
921 param = params[:include]
917 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
922 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
918 @included_in_api_response.collect!(&:strip)
923 @included_in_api_response.collect!(&:strip)
919 end
924 end
920 @included_in_api_response.include?(arg.to_s)
925 @included_in_api_response.include?(arg.to_s)
921 end
926 end
922
927
923 # Returns options or nil if nometa param or X-Redmine-Nometa header
928 # Returns options or nil if nometa param or X-Redmine-Nometa header
924 # was set in the request
929 # was set in the request
925 def api_meta(options)
930 def api_meta(options)
926 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
931 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
927 # compatibility mode for activeresource clients that raise
932 # compatibility mode for activeresource clients that raise
928 # an error when unserializing an array with attributes
933 # an error when unserializing an array with attributes
929 nil
934 nil
930 else
935 else
931 options
936 options
932 end
937 end
933 end
938 end
934
939
935 private
940 private
936
941
937 def wiki_helper
942 def wiki_helper
938 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
943 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
939 extend helper
944 extend helper
940 return self
945 return self
941 end
946 end
942
947
943 def link_to_content_update(text, url_params = {}, html_options = {})
948 def link_to_content_update(text, url_params = {}, html_options = {})
944 link_to(text, url_params, html_options)
949 link_to(text, url_params, html_options)
945 end
950 end
946 end
951 end
@@ -1,725 +1,735
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../../test_helper', __FILE__)
18 require File.expand_path('../../../test_helper', __FILE__)
19
19
20 class ApplicationHelperTest < ActionView::TestCase
20 class ApplicationHelperTest < ActionView::TestCase
21
21
22 fixtures :projects, :roles, :enabled_modules, :users,
22 fixtures :projects, :roles, :enabled_modules, :users,
23 :repositories, :changesets,
23 :repositories, :changesets,
24 :trackers, :issue_statuses, :issues, :versions, :documents,
24 :trackers, :issue_statuses, :issues, :versions, :documents,
25 :wikis, :wiki_pages, :wiki_contents,
25 :wikis, :wiki_pages, :wiki_contents,
26 :boards, :messages,
26 :boards, :messages,
27 :attachments,
27 :attachments,
28 :enumerations
28 :enumerations
29
29
30 def setup
30 def setup
31 super
31 super
32 end
32 end
33
33
34 context "#link_to_if_authorized" do
34 context "#link_to_if_authorized" do
35 context "authorized user" do
35 context "authorized user" do
36 should "be tested"
36 should "be tested"
37 end
37 end
38
38
39 context "unauthorized user" do
39 context "unauthorized user" do
40 should "be tested"
40 should "be tested"
41 end
41 end
42
42
43 should "allow using the :controller and :action for the target link" do
43 should "allow using the :controller and :action for the target link" do
44 User.current = User.find_by_login('admin')
44 User.current = User.find_by_login('admin')
45
45
46 @project = Issue.first.project # Used by helper
46 @project = Issue.first.project # Used by helper
47 response = link_to_if_authorized("By controller/action",
47 response = link_to_if_authorized("By controller/action",
48 {:controller => 'issues', :action => 'edit', :id => Issue.first.id})
48 {:controller => 'issues', :action => 'edit', :id => Issue.first.id})
49 assert_match /href/, response
49 assert_match /href/, response
50 end
50 end
51
51
52 end
52 end
53
53
54 def test_auto_links
54 def test_auto_links
55 to_test = {
55 to_test = {
56 'http://foo.bar' => '<a class="external" href="http://foo.bar">http://foo.bar</a>',
56 'http://foo.bar' => '<a class="external" href="http://foo.bar">http://foo.bar</a>',
57 'http://foo.bar/~user' => '<a class="external" href="http://foo.bar/~user">http://foo.bar/~user</a>',
57 'http://foo.bar/~user' => '<a class="external" href="http://foo.bar/~user">http://foo.bar/~user</a>',
58 'http://foo.bar.' => '<a class="external" href="http://foo.bar">http://foo.bar</a>.',
58 'http://foo.bar.' => '<a class="external" href="http://foo.bar">http://foo.bar</a>.',
59 'https://foo.bar.' => '<a class="external" href="https://foo.bar">https://foo.bar</a>.',
59 'https://foo.bar.' => '<a class="external" href="https://foo.bar">https://foo.bar</a>.',
60 'This is a link: http://foo.bar.' => 'This is a link: <a class="external" href="http://foo.bar">http://foo.bar</a>.',
60 'This is a link: http://foo.bar.' => 'This is a link: <a class="external" href="http://foo.bar">http://foo.bar</a>.',
61 'A link (eg. http://foo.bar).' => 'A link (eg. <a class="external" href="http://foo.bar">http://foo.bar</a>).',
61 'A link (eg. http://foo.bar).' => 'A link (eg. <a class="external" href="http://foo.bar">http://foo.bar</a>).',
62 '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>.',
62 '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>.',
63 'http://www.foo.bar/Test_(foobar)' => '<a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>',
63 'http://www.foo.bar/Test_(foobar)' => '<a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>',
64 '(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>)',
64 '(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>)',
65 '(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>)',
65 '(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>)',
66 '(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>).',
66 '(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>).',
67 '(see "inline link":http://www.foo.bar/Test_(foobar))' => '(see <a href="http://www.foo.bar/Test_(foobar)" class="external">inline link</a>)',
67 '(see "inline link":http://www.foo.bar/Test_(foobar))' => '(see <a href="http://www.foo.bar/Test_(foobar)" class="external">inline link</a>)',
68 '(see "inline link":http://www.foo.bar/Test)' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>)',
68 '(see "inline link":http://www.foo.bar/Test)' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>)',
69 '(see "inline link":http://www.foo.bar/Test).' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>).',
69 '(see "inline link":http://www.foo.bar/Test).' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>).',
70 'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>',
70 'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>',
71 '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>',
71 '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>',
72 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>',
72 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>',
73 'http://foo@www.bar.com' => '<a class="external" href="http://foo@www.bar.com">http://foo@www.bar.com</a>',
73 'http://foo@www.bar.com' => '<a class="external" href="http://foo@www.bar.com">http://foo@www.bar.com</a>',
74 'http://foo:bar@www.bar.com' => '<a class="external" href="http://foo:bar@www.bar.com">http://foo:bar@www.bar.com</a>',
74 'http://foo:bar@www.bar.com' => '<a class="external" href="http://foo:bar@www.bar.com">http://foo:bar@www.bar.com</a>',
75 'ftp://foo.bar' => '<a class="external" href="ftp://foo.bar">ftp://foo.bar</a>',
75 'ftp://foo.bar' => '<a class="external" href="ftp://foo.bar">ftp://foo.bar</a>',
76 'ftps://foo.bar' => '<a class="external" href="ftps://foo.bar">ftps://foo.bar</a>',
76 'ftps://foo.bar' => '<a class="external" href="ftps://foo.bar">ftps://foo.bar</a>',
77 'sftp://foo.bar' => '<a class="external" href="sftp://foo.bar">sftp://foo.bar</a>',
77 'sftp://foo.bar' => '<a class="external" href="sftp://foo.bar">sftp://foo.bar</a>',
78 # two exclamation marks
78 # two exclamation marks
79 '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>',
79 '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>',
80 # escaping
80 # escaping
81 'http://foo"bar' => '<a class="external" href="http://foo&quot;bar">http://foo"bar</a>',
81 'http://foo"bar' => '<a class="external" href="http://foo&quot;bar">http://foo"bar</a>',
82 # wrap in angle brackets
82 # wrap in angle brackets
83 '<http://foo.bar>' => '&lt;<a class="external" href="http://foo.bar">http://foo.bar</a>&gt;'
83 '<http://foo.bar>' => '&lt;<a class="external" href="http://foo.bar">http://foo.bar</a>&gt;'
84 }
84 }
85 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
85 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
86 end
86 end
87
87
88 def test_auto_mailto
88 def test_auto_mailto
89 assert_equal '<p><a class="email" href="mailto:test@foo.bar">test@foo.bar</a></p>',
89 assert_equal '<p><a class="email" href="mailto:test@foo.bar">test@foo.bar</a></p>',
90 textilizable('test@foo.bar')
90 textilizable('test@foo.bar')
91 end
91 end
92
92
93 def test_inline_images
93 def test_inline_images
94 to_test = {
94 to_test = {
95 '!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
95 '!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
96 'floating !>http://foo.bar/image.jpg!' => 'floating <div style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></div>',
96 'floating !>http://foo.bar/image.jpg!' => 'floating <div style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></div>',
97 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class <img src="http://foo.bar/image.jpg" class="some-class" alt="" />',
97 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class <img src="http://foo.bar/image.jpg" class="some-class" alt="" />',
98 # inline styles should be stripped
98 # inline styles should be stripped
99 'with style !{width:100px;height100px}http://foo.bar/image.jpg!' => 'with style <img src="http://foo.bar/image.jpg" alt="" />',
99 'with style !{width:100px;height100px}http://foo.bar/image.jpg!' => 'with style <img src="http://foo.bar/image.jpg" alt="" />',
100 '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" />',
100 '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" />',
101 '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;" />',
101 '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;" />',
102 }
102 }
103 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
103 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
104 end
104 end
105
105
106 def test_inline_images_inside_tags
106 def test_inline_images_inside_tags
107 raw = <<-RAW
107 raw = <<-RAW
108 h1. !foo.png! Heading
108 h1. !foo.png! Heading
109
109
110 Centered image:
110 Centered image:
111
111
112 p=. !bar.gif!
112 p=. !bar.gif!
113 RAW
113 RAW
114
114
115 assert textilizable(raw).include?('<img src="foo.png" alt="" />')
115 assert textilizable(raw).include?('<img src="foo.png" alt="" />')
116 assert textilizable(raw).include?('<img src="bar.gif" alt="" />')
116 assert textilizable(raw).include?('<img src="bar.gif" alt="" />')
117 end
117 end
118
118
119 def test_attached_images
119 def test_attached_images
120 to_test = {
120 to_test = {
121 'Inline image: !logo.gif!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
121 'Inline image: !logo.gif!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
122 'Inline image: !logo.GIF!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
122 'Inline image: !logo.GIF!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
123 'No match: !ogo.gif!' => 'No match: <img src="ogo.gif" alt="" />',
123 'No match: !ogo.gif!' => 'No match: <img src="ogo.gif" alt="" />',
124 'No match: !ogo.GIF!' => 'No match: <img src="ogo.GIF" alt="" />',
124 'No match: !ogo.GIF!' => 'No match: <img src="ogo.GIF" alt="" />',
125 # link image
125 # link image
126 '!logo.gif!:http://foo.bar/' => '<a href="http://foo.bar/"><img src="/attachments/download/3" title="This is a logo" alt="This is a logo" /></a>',
126 '!logo.gif!:http://foo.bar/' => '<a href="http://foo.bar/"><img src="/attachments/download/3" title="This is a logo" alt="This is a logo" /></a>',
127 }
127 }
128 attachments = Attachment.find(:all)
128 attachments = Attachment.find(:all)
129 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
129 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
130 end
130 end
131
131
132 def test_textile_external_links
132 def test_textile_external_links
133 to_test = {
133 to_test = {
134 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>',
134 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>',
135 'This is an intern "link":/foo/bar' => 'This is an intern <a href="/foo/bar">link</a>',
135 'This is an intern "link":/foo/bar' => 'This is an intern <a href="/foo/bar">link</a>',
136 '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>',
136 '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>',
137 '"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>',
137 '"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>',
138 "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":</p>\n\n\n\t<p>Another paragraph",
138 "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":</p>\n\n\n\t<p>Another paragraph",
139 # no multiline link text
139 # no multiline link text
140 "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",
140 "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",
141 # mailto link
141 # mailto link
142 "\"system administrator\":mailto:sysadmin@example.com?subject=redmine%20permissions" => "<a href=\"mailto:sysadmin@example.com?subject=redmine%20permissions\">system administrator</a>",
142 "\"system administrator\":mailto:sysadmin@example.com?subject=redmine%20permissions" => "<a href=\"mailto:sysadmin@example.com?subject=redmine%20permissions\">system administrator</a>",
143 # two exclamation marks
143 # two exclamation marks
144 '"a link":http://example.net/path!602815048C7B5C20!302.html' => '<a href="http://example.net/path!602815048C7B5C20!302.html" class="external">a link</a>',
144 '"a link":http://example.net/path!602815048C7B5C20!302.html' => '<a href="http://example.net/path!602815048C7B5C20!302.html" class="external">a link</a>',
145 # escaping
145 # escaping
146 '"test":http://foo"bar' => '<a href="http://foo&quot;bar" class="external">test</a>',
146 '"test":http://foo"bar' => '<a href="http://foo&quot;bar" class="external">test</a>',
147 }
147 }
148 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
148 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
149 end
149 end
150
150
151 def test_redmine_links
151 def test_redmine_links
152 issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3},
152 issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3},
153 :class => 'issue status-1 priority-1 overdue', :title => 'Error 281 when updating a recipe (New)')
153 :class => 'issue status-1 priority-1 overdue', :title => 'Error 281 when updating a recipe (New)')
154
154
155 changeset_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
155 changeset_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
156 :class => 'changeset', :title => 'My very first commit')
156 :class => 'changeset', :title => 'My very first commit')
157 changeset_link2 = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
157 changeset_link2 = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
158 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
158 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
159
159
160 document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
160 document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
161 :class => 'document')
161 :class => 'document')
162
162
163 version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
163 version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
164 :class => 'version')
164 :class => 'version')
165
165
166 message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4}
166 message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4}
167
167
168 project_url = {:controller => 'projects', :action => 'show', :id => 'subproject1'}
168 project_url = {:controller => 'projects', :action => 'show', :id => 'subproject1'}
169
169
170 source_url = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}
170 source_url = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}
171 source_url_with_ext = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file.ext']}
171 source_url_with_ext = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file.ext']}
172
172
173 to_test = {
173 to_test = {
174 # tickets
174 # tickets
175 '#3, [#3], (#3) and #3.' => "#{issue_link}, [#{issue_link}], (#{issue_link}) and #{issue_link}.",
175 '#3, [#3], (#3) and #3.' => "#{issue_link}, [#{issue_link}], (#{issue_link}) and #{issue_link}.",
176 # changesets
176 # changesets
177 'r1' => changeset_link,
177 'r1' => changeset_link,
178 'r1.' => "#{changeset_link}.",
178 'r1.' => "#{changeset_link}.",
179 'r1, r2' => "#{changeset_link}, #{changeset_link2}",
179 'r1, r2' => "#{changeset_link}, #{changeset_link2}",
180 'r1,r2' => "#{changeset_link},#{changeset_link2}",
180 'r1,r2' => "#{changeset_link},#{changeset_link2}",
181 # documents
181 # documents
182 'document#1' => document_link,
182 'document#1' => document_link,
183 'document:"Test document"' => document_link,
183 'document:"Test document"' => document_link,
184 # versions
184 # versions
185 'version#2' => version_link,
185 'version#2' => version_link,
186 'version:1.0' => version_link,
186 'version:1.0' => version_link,
187 'version:"1.0"' => version_link,
187 'version:"1.0"' => version_link,
188 # source
188 # source
189 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
189 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
190 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".",
190 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".",
191 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
191 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
192 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".",
192 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".",
193 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
193 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
194 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",",
194 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",",
195 'source:/some/file@52' => link_to('source:/some/file@52', source_url.merge(:rev => 52), :class => 'source'),
195 'source:/some/file@52' => link_to('source:/some/file@52', source_url.merge(:rev => 52), :class => 'source'),
196 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_ext.merge(:rev => 52), :class => 'source'),
196 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_ext.merge(:rev => 52), :class => 'source'),
197 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url.merge(:anchor => 'L110'), :class => 'source'),
197 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url.merge(:anchor => 'L110'), :class => 'source'),
198 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext.merge(:anchor => 'L110'), :class => 'source'),
198 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext.merge(:anchor => 'L110'), :class => 'source'),
199 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url.merge(:rev => 52, :anchor => 'L110'), :class => 'source'),
199 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url.merge(:rev => 52, :anchor => 'L110'), :class => 'source'),
200 'export:/some/file' => link_to('export:/some/file', source_url.merge(:format => 'raw'), :class => 'source download'),
200 'export:/some/file' => link_to('export:/some/file', source_url.merge(:format => 'raw'), :class => 'source download'),
201 # message
201 # message
202 'message#4' => link_to('Post 2', message_url, :class => 'message'),
202 'message#4' => link_to('Post 2', message_url, :class => 'message'),
203 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5', :r => 5), :class => 'message'),
203 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5', :r => 5), :class => 'message'),
204 # project
204 # project
205 'project#3' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
205 'project#3' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
206 'project:subproject1' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
206 'project:subproject1' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
207 'project:"eCookbook subProject 1"' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
207 'project:"eCookbook subProject 1"' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
208 # escaping
208 # escaping
209 '!#3.' => '#3.',
209 '!#3.' => '#3.',
210 '!r1' => 'r1',
210 '!r1' => 'r1',
211 '!document#1' => 'document#1',
211 '!document#1' => 'document#1',
212 '!document:"Test document"' => 'document:"Test document"',
212 '!document:"Test document"' => 'document:"Test document"',
213 '!version#2' => 'version#2',
213 '!version#2' => 'version#2',
214 '!version:1.0' => 'version:1.0',
214 '!version:1.0' => 'version:1.0',
215 '!version:"1.0"' => 'version:"1.0"',
215 '!version:"1.0"' => 'version:"1.0"',
216 '!source:/some/file' => 'source:/some/file',
216 '!source:/some/file' => 'source:/some/file',
217 # not found
217 # not found
218 '#0123456789' => '#0123456789',
218 '#0123456789' => '#0123456789',
219 # invalid expressions
219 # invalid expressions
220 'source:' => 'source:',
220 'source:' => 'source:',
221 # url hash
221 # url hash
222 "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
222 "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
223 }
223 }
224 @project = Project.find(1)
224 @project = Project.find(1)
225 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
225 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
226 end
226 end
227
227
228 def test_cross_project_redmine_links
228 def test_cross_project_redmine_links
229 source_link = link_to('ecookbook:source:/some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']},
229 source_link = link_to('ecookbook:source:/some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']},
230 :class => 'source')
230 :class => 'source')
231
231
232 changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
232 changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
233 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
233 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
234
234
235 to_test = {
235 to_test = {
236 # documents
236 # documents
237 'document:"Test document"' => 'document:"Test document"',
237 'document:"Test document"' => 'document:"Test document"',
238 'ecookbook:document:"Test document"' => '<a href="/documents/1" class="document">Test document</a>',
238 'ecookbook:document:"Test document"' => '<a href="/documents/1" class="document">Test document</a>',
239 'invalid:document:"Test document"' => 'invalid:document:"Test document"',
239 'invalid:document:"Test document"' => 'invalid:document:"Test document"',
240 # versions
240 # versions
241 'version:"1.0"' => 'version:"1.0"',
241 'version:"1.0"' => 'version:"1.0"',
242 'ecookbook:version:"1.0"' => '<a href="/versions/show/2" class="version">1.0</a>',
242 'ecookbook:version:"1.0"' => '<a href="/versions/show/2" class="version">1.0</a>',
243 'invalid:version:"1.0"' => 'invalid:version:"1.0"',
243 'invalid:version:"1.0"' => 'invalid:version:"1.0"',
244 # changeset
244 # changeset
245 'r2' => 'r2',
245 'r2' => 'r2',
246 'ecookbook:r2' => changeset_link,
246 'ecookbook:r2' => changeset_link,
247 'invalid:r2' => 'invalid:r2',
247 'invalid:r2' => 'invalid:r2',
248 # source
248 # source
249 'source:/some/file' => 'source:/some/file',
249 'source:/some/file' => 'source:/some/file',
250 'ecookbook:source:/some/file' => source_link,
250 'ecookbook:source:/some/file' => source_link,
251 'invalid:source:/some/file' => 'invalid:source:/some/file',
251 'invalid:source:/some/file' => 'invalid:source:/some/file',
252 }
252 }
253 @project = Project.find(3)
253 @project = Project.find(3)
254 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
254 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
255 end
255 end
256
256
257 def test_redmine_links_git_commit
257 def test_redmine_links_git_commit
258 changeset_link = link_to('abcd',
258 changeset_link = link_to('abcd',
259 {
259 {
260 :controller => 'repositories',
260 :controller => 'repositories',
261 :action => 'revision',
261 :action => 'revision',
262 :id => 'subproject1',
262 :id => 'subproject1',
263 :rev => 'abcd',
263 :rev => 'abcd',
264 },
264 },
265 :class => 'changeset', :title => 'test commit')
265 :class => 'changeset', :title => 'test commit')
266 to_test = {
266 to_test = {
267 'commit:abcd' => changeset_link,
267 'commit:abcd' => changeset_link,
268 }
268 }
269 @project = Project.find(3)
269 @project = Project.find(3)
270 r = Repository::Git.create!(:project => @project, :url => '/tmp/test/git')
270 r = Repository::Git.create!(:project => @project, :url => '/tmp/test/git')
271 assert r
271 assert r
272 c = Changeset.new(:repository => r,
272 c = Changeset.new(:repository => r,
273 :committed_on => Time.now,
273 :committed_on => Time.now,
274 :revision => 'abcd',
274 :revision => 'abcd',
275 :scmid => 'abcd',
275 :scmid => 'abcd',
276 :comments => 'test commit')
276 :comments => 'test commit')
277 assert( c.save )
277 assert( c.save )
278 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
278 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
279 end
279 end
280
280
281 # TODO: Bazaar commit id contains mail address, so it contains '@' and '_'.
281 # TODO: Bazaar commit id contains mail address, so it contains '@' and '_'.
282 def test_redmine_links_darcs_commit
282 def test_redmine_links_darcs_commit
283 changeset_link = link_to('20080308225258-98289-abcd456efg.gz',
283 changeset_link = link_to('20080308225258-98289-abcd456efg.gz',
284 {
284 {
285 :controller => 'repositories',
285 :controller => 'repositories',
286 :action => 'revision',
286 :action => 'revision',
287 :id => 'subproject1',
287 :id => 'subproject1',
288 :rev => '123',
288 :rev => '123',
289 },
289 },
290 :class => 'changeset', :title => 'test commit')
290 :class => 'changeset', :title => 'test commit')
291 to_test = {
291 to_test = {
292 'commit:20080308225258-98289-abcd456efg.gz' => changeset_link,
292 'commit:20080308225258-98289-abcd456efg.gz' => changeset_link,
293 }
293 }
294 @project = Project.find(3)
294 @project = Project.find(3)
295 r = Repository::Darcs.create!(
295 r = Repository::Darcs.create!(
296 :project => @project, :url => '/tmp/test/darcs',
296 :project => @project, :url => '/tmp/test/darcs',
297 :log_encoding => 'UTF-8')
297 :log_encoding => 'UTF-8')
298 assert r
298 assert r
299 c = Changeset.new(:repository => r,
299 c = Changeset.new(:repository => r,
300 :committed_on => Time.now,
300 :committed_on => Time.now,
301 :revision => '123',
301 :revision => '123',
302 :scmid => '20080308225258-98289-abcd456efg.gz',
302 :scmid => '20080308225258-98289-abcd456efg.gz',
303 :comments => 'test commit')
303 :comments => 'test commit')
304 assert( c.save )
304 assert( c.save )
305 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
305 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
306 end
306 end
307
307
308 def test_redmine_links_mercurial_commit
308 def test_redmine_links_mercurial_commit
309 changeset_link_rev = link_to('r123',
309 changeset_link_rev = link_to('r123',
310 {
310 {
311 :controller => 'repositories',
311 :controller => 'repositories',
312 :action => 'revision',
312 :action => 'revision',
313 :id => 'subproject1',
313 :id => 'subproject1',
314 :rev => '123' ,
314 :rev => '123' ,
315 },
315 },
316 :class => 'changeset', :title => 'test commit')
316 :class => 'changeset', :title => 'test commit')
317 changeset_link_commit = link_to('abcd',
317 changeset_link_commit = link_to('abcd',
318 {
318 {
319 :controller => 'repositories',
319 :controller => 'repositories',
320 :action => 'revision',
320 :action => 'revision',
321 :id => 'subproject1',
321 :id => 'subproject1',
322 :rev => 'abcd' ,
322 :rev => 'abcd' ,
323 },
323 },
324 :class => 'changeset', :title => 'test commit')
324 :class => 'changeset', :title => 'test commit')
325 to_test = {
325 to_test = {
326 'r123' => changeset_link_rev,
326 'r123' => changeset_link_rev,
327 'commit:abcd' => changeset_link_commit,
327 'commit:abcd' => changeset_link_commit,
328 }
328 }
329 @project = Project.find(3)
329 @project = Project.find(3)
330 r = Repository::Mercurial.create!(:project => @project, :url => '/tmp/test')
330 r = Repository::Mercurial.create!(:project => @project, :url => '/tmp/test')
331 assert r
331 assert r
332 c = Changeset.new(:repository => r,
332 c = Changeset.new(:repository => r,
333 :committed_on => Time.now,
333 :committed_on => Time.now,
334 :revision => '123',
334 :revision => '123',
335 :scmid => 'abcd',
335 :scmid => 'abcd',
336 :comments => 'test commit')
336 :comments => 'test commit')
337 assert( c.save )
337 assert( c.save )
338 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
338 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
339 end
339 end
340
340
341 def test_attachment_links
341 def test_attachment_links
342 attachment_link = link_to('error281.txt', {:controller => 'attachments', :action => 'download', :id => '1'}, :class => 'attachment')
342 attachment_link = link_to('error281.txt', {:controller => 'attachments', :action => 'download', :id => '1'}, :class => 'attachment')
343 to_test = {
343 to_test = {
344 'attachment:error281.txt' => attachment_link
344 'attachment:error281.txt' => attachment_link
345 }
345 }
346 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => Issue.find(3).attachments), "#{text} failed" }
346 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => Issue.find(3).attachments), "#{text} failed" }
347 end
347 end
348
348
349 def test_wiki_links
349 def test_wiki_links
350 to_test = {
350 to_test = {
351 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
351 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
352 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
352 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
353 # link with anchor
353 # link with anchor
354 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
354 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
355 '[[Another page#anchor|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page#anchor" class="wiki-page">Page</a>',
355 '[[Another page#anchor|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page#anchor" class="wiki-page">Page</a>',
356 # page that doesn't exist
356 # page that doesn't exist
357 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
357 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
358 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">404</a>',
358 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">404</a>',
359 # link to another project wiki
359 # link to another project wiki
360 '[[onlinestore:]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">onlinestore</a>',
360 '[[onlinestore:]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">onlinestore</a>',
361 '[[onlinestore:|Wiki]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">Wiki</a>',
361 '[[onlinestore:|Wiki]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">Wiki</a>',
362 '[[onlinestore:Start page]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Start page</a>',
362 '[[onlinestore:Start page]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Start page</a>',
363 '[[onlinestore:Start page|Text]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Text</a>',
363 '[[onlinestore:Start page|Text]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Text</a>',
364 '[[onlinestore:Unknown page]]' => '<a href="/projects/onlinestore/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
364 '[[onlinestore:Unknown page]]' => '<a href="/projects/onlinestore/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
365 # striked through link
365 # striked through link
366 '-[[Another page|Page]]-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a></del>',
366 '-[[Another page|Page]]-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a></del>',
367 '-[[Another page|Page]] link-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a> link</del>',
367 '-[[Another page|Page]] link-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a> link</del>',
368 # escaping
368 # escaping
369 '![[Another page|Page]]' => '[[Another page|Page]]',
369 '![[Another page|Page]]' => '[[Another page|Page]]',
370 # project does not exist
370 # project does not exist
371 '[[unknowproject:Start]]' => '[[unknowproject:Start]]',
371 '[[unknowproject:Start]]' => '[[unknowproject:Start]]',
372 '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]',
372 '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]',
373 }
373 }
374 @project = Project.find(1)
374 @project = Project.find(1)
375 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
375 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
376 end
376 end
377
377
378 def test_wiki_links_within_local_file_generation_context
378 def test_wiki_links_within_local_file_generation_context
379
379
380 to_test = {
380 to_test = {
381 # link to a page
381 # link to a page
382 '[[CookBook documentation]]' => '<a href="CookBook_documentation.html" class="wiki-page">CookBook documentation</a>',
382 '[[CookBook documentation]]' => '<a href="CookBook_documentation.html" class="wiki-page">CookBook documentation</a>',
383 '[[CookBook documentation|documentation]]' => '<a href="CookBook_documentation.html" class="wiki-page">documentation</a>',
383 '[[CookBook documentation|documentation]]' => '<a href="CookBook_documentation.html" class="wiki-page">documentation</a>',
384 '[[CookBook documentation#One-section]]' => '<a href="CookBook_documentation.html#One-section" class="wiki-page">CookBook documentation</a>',
384 '[[CookBook documentation#One-section]]' => '<a href="CookBook_documentation.html#One-section" class="wiki-page">CookBook documentation</a>',
385 '[[CookBook documentation#One-section|documentation]]' => '<a href="CookBook_documentation.html#One-section" class="wiki-page">documentation</a>',
385 '[[CookBook documentation#One-section|documentation]]' => '<a href="CookBook_documentation.html#One-section" class="wiki-page">documentation</a>',
386 # page that doesn't exist
386 # page that doesn't exist
387 '[[Unknown page]]' => '<a href="Unknown_page.html" class="wiki-page new">Unknown page</a>',
387 '[[Unknown page]]' => '<a href="Unknown_page.html" class="wiki-page new">Unknown page</a>',
388 '[[Unknown page|404]]' => '<a href="Unknown_page.html" class="wiki-page new">404</a>',
388 '[[Unknown page|404]]' => '<a href="Unknown_page.html" class="wiki-page new">404</a>',
389 '[[Unknown page#anchor]]' => '<a href="Unknown_page.html#anchor" class="wiki-page new">Unknown page</a>',
389 '[[Unknown page#anchor]]' => '<a href="Unknown_page.html#anchor" class="wiki-page new">Unknown page</a>',
390 '[[Unknown page#anchor|404]]' => '<a href="Unknown_page.html#anchor" class="wiki-page new">404</a>',
390 '[[Unknown page#anchor|404]]' => '<a href="Unknown_page.html#anchor" class="wiki-page new">404</a>',
391 }
391 }
392
392
393 @project = Project.find(1)
393 @project = Project.find(1)
394
394
395 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :local) }
395 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :local) }
396 end
396 end
397
397
398 def test_html_tags
398 def test_html_tags
399 to_test = {
399 to_test = {
400 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
400 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
401 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
401 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
402 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
402 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
403 # do not escape pre/code tags
403 # do not escape pre/code tags
404 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
404 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
405 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
405 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
406 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
406 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
407 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
407 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
408 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
408 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
409 # remove attributes except class
409 # remove attributes except class
410 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
410 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
411 '<pre class="foo">some text</pre>' => '<pre class="foo">some text</pre>',
411 '<pre class="foo">some text</pre>' => '<pre class="foo">some text</pre>',
412 "<pre class='foo bar'>some text</pre>" => "<pre class='foo bar'>some text</pre>",
412 "<pre class='foo bar'>some text</pre>" => "<pre class='foo bar'>some text</pre>",
413 '<pre class="foo bar">some text</pre>' => '<pre class="foo bar">some text</pre>',
413 '<pre class="foo bar">some text</pre>' => '<pre class="foo bar">some text</pre>',
414 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
414 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
415 # xss
415 # xss
416 '<pre><code class=""onmouseover="alert(1)">text</code></pre>' => '<pre><code>text</code></pre>',
416 '<pre><code class=""onmouseover="alert(1)">text</code></pre>' => '<pre><code>text</code></pre>',
417 '<pre class=""onmouseover="alert(1)">text</pre>' => '<pre>text</pre>',
417 '<pre class=""onmouseover="alert(1)">text</pre>' => '<pre>text</pre>',
418 }
418 }
419 to_test.each { |text, result| assert_equal result, textilizable(text) }
419 to_test.each { |text, result| assert_equal result, textilizable(text) }
420 end
420 end
421
421
422 def test_allowed_html_tags
422 def test_allowed_html_tags
423 to_test = {
423 to_test = {
424 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
424 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
425 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
425 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
426 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
426 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
427 }
427 }
428 to_test.each { |text, result| assert_equal result, textilizable(text) }
428 to_test.each { |text, result| assert_equal result, textilizable(text) }
429 end
429 end
430
430
431 def test_pre_tags
431 def test_pre_tags
432 raw = <<-RAW
432 raw = <<-RAW
433 Before
433 Before
434
434
435 <pre>
435 <pre>
436 <prepared-statement-cache-size>32</prepared-statement-cache-size>
436 <prepared-statement-cache-size>32</prepared-statement-cache-size>
437 </pre>
437 </pre>
438
438
439 After
439 After
440 RAW
440 RAW
441
441
442 expected = <<-EXPECTED
442 expected = <<-EXPECTED
443 <p>Before</p>
443 <p>Before</p>
444 <pre>
444 <pre>
445 &lt;prepared-statement-cache-size&gt;32&lt;/prepared-statement-cache-size&gt;
445 &lt;prepared-statement-cache-size&gt;32&lt;/prepared-statement-cache-size&gt;
446 </pre>
446 </pre>
447 <p>After</p>
447 <p>After</p>
448 EXPECTED
448 EXPECTED
449
449
450 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
450 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
451 end
451 end
452
452
453 def test_pre_content_should_not_parse_wiki_and_redmine_links
453 def test_pre_content_should_not_parse_wiki_and_redmine_links
454 raw = <<-RAW
454 raw = <<-RAW
455 [[CookBook documentation]]
455 [[CookBook documentation]]
456
456
457 #1
457 #1
458
458
459 <pre>
459 <pre>
460 [[CookBook documentation]]
460 [[CookBook documentation]]
461
461
462 #1
462 #1
463 </pre>
463 </pre>
464 RAW
464 RAW
465
465
466 expected = <<-EXPECTED
466 expected = <<-EXPECTED
467 <p><a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a></p>
467 <p><a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a></p>
468 <p><a href="/issues/1" class="issue status-1 priority-1" title="Can't print recipes (New)">#1</a></p>
468 <p><a href="/issues/1" class="issue status-1 priority-1" title="Can't print recipes (New)">#1</a></p>
469 <pre>
469 <pre>
470 [[CookBook documentation]]
470 [[CookBook documentation]]
471
471
472 #1
472 #1
473 </pre>
473 </pre>
474 EXPECTED
474 EXPECTED
475
475
476 @project = Project.find(1)
476 @project = Project.find(1)
477 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
477 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
478 end
478 end
479
479
480 def test_non_closing_pre_blocks_should_be_closed
480 def test_non_closing_pre_blocks_should_be_closed
481 raw = <<-RAW
481 raw = <<-RAW
482 <pre><code>
482 <pre><code>
483 RAW
483 RAW
484
484
485 expected = <<-EXPECTED
485 expected = <<-EXPECTED
486 <pre><code>
486 <pre><code>
487 </code></pre>
487 </code></pre>
488 EXPECTED
488 EXPECTED
489
489
490 @project = Project.find(1)
490 @project = Project.find(1)
491 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
491 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
492 end
492 end
493
493
494 def test_syntax_highlight
494 def test_syntax_highlight
495 raw = <<-RAW
495 raw = <<-RAW
496 <pre><code class="ruby">
496 <pre><code class="ruby">
497 # Some ruby code here
497 # Some ruby code here
498 </code></pre>
498 </code></pre>
499 RAW
499 RAW
500
500
501 expected = <<-EXPECTED
501 expected = <<-EXPECTED
502 <pre><code class="ruby syntaxhl"><span class=\"CodeRay\"><span class="no">1</span> <span class="c"># Some ruby code here</span></span>
502 <pre><code class="ruby syntaxhl"><span class=\"CodeRay\"><span class="no">1</span> <span class="c"># Some ruby code here</span></span>
503 </code></pre>
503 </code></pre>
504 EXPECTED
504 EXPECTED
505
505
506 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
506 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
507 end
507 end
508
508
509 def test_wiki_links_in_tables
509 def test_wiki_links_in_tables
510 to_test = {"|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" =>
510 to_test = {"|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" =>
511 '<tr><td><a href="/projects/ecookbook/wiki/Page" class="wiki-page new">Link title</a></td>' +
511 '<tr><td><a href="/projects/ecookbook/wiki/Page" class="wiki-page new">Link title</a></td>' +
512 '<td><a href="/projects/ecookbook/wiki/Other_Page" class="wiki-page new">Other title</a></td>' +
512 '<td><a href="/projects/ecookbook/wiki/Other_Page" class="wiki-page new">Other title</a></td>' +
513 '</tr><tr><td>Cell 21</td><td><a href="/projects/ecookbook/wiki/Last_page" class="wiki-page new">Last page</a></td></tr>'
513 '</tr><tr><td>Cell 21</td><td><a href="/projects/ecookbook/wiki/Last_page" class="wiki-page new">Last page</a></td></tr>'
514 }
514 }
515 @project = Project.find(1)
515 @project = Project.find(1)
516 to_test.each { |text, result| assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '') }
516 to_test.each { |text, result| assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '') }
517 end
517 end
518
518
519 def test_text_formatting
519 def test_text_formatting
520 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
520 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
521 '(_text within parentheses_)' => '(<em>text within parentheses</em>)',
521 '(_text within parentheses_)' => '(<em>text within parentheses</em>)',
522 'a *Humane Web* Text Generator' => 'a <strong>Humane Web</strong> Text Generator',
522 'a *Humane Web* Text Generator' => 'a <strong>Humane Web</strong> Text Generator',
523 '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>',
523 '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>',
524 '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',
524 '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',
525 }
525 }
526 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
526 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
527 end
527 end
528
528
529 def test_wiki_horizontal_rule
529 def test_wiki_horizontal_rule
530 assert_equal '<hr />', textilizable('---')
530 assert_equal '<hr />', textilizable('---')
531 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
531 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
532 end
532 end
533
533
534 def test_footnotes
534 def test_footnotes
535 raw = <<-RAW
535 raw = <<-RAW
536 This is some text[1].
536 This is some text[1].
537
537
538 fn1. This is the foot note
538 fn1. This is the foot note
539 RAW
539 RAW
540
540
541 expected = <<-EXPECTED
541 expected = <<-EXPECTED
542 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
542 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
543 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
543 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
544 EXPECTED
544 EXPECTED
545
545
546 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
546 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
547 end
547 end
548
548
549 def test_headings
549 def test_headings
550 raw = 'h1. Some heading'
550 raw = 'h1. Some heading'
551 expected = %|<a name="Some-heading"></a>\n<h1 >Some heading<a href="#Some-heading" class="wiki-anchor">&para;</a></h1>|
551 expected = %|<a name="Some-heading"></a>\n<h1 >Some heading<a href="#Some-heading" class="wiki-anchor">&para;</a></h1>|
552
552
553 assert_equal expected, textilizable(raw)
553 assert_equal expected, textilizable(raw)
554 end
554 end
555
555
556 def test_headings_with_special_chars
557 # This test makes sure that the generated anchor names match the expected
558 # ones even if the heading text contains unconventional characters
559 raw = 'h1. Some heading related to version 0.5'
560 anchor = sanitize_anchor_name("Some-heading-related-to-version-0.5")
561 expected = %|<a name="#{anchor}"></a>\n<h1 >Some heading related to version 0.5<a href="##{anchor}" class="wiki-anchor">&para;</a></h1>|
562
563 assert_equal expected, textilizable(raw)
564 end
565
556 def test_wiki_links_within_wiki_page_context
566 def test_wiki_links_within_wiki_page_context
557
567
558 page = WikiPage.find_by_title('Another_page' )
568 page = WikiPage.find_by_title('Another_page' )
559
569
560 to_test = {
570 to_test = {
561 # link to another page
571 # link to another page
562 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
572 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
563 '[[CookBook documentation|documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">documentation</a>',
573 '[[CookBook documentation|documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">documentation</a>',
564 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
574 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
565 '[[CookBook documentation#One-section|documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">documentation</a>',
575 '[[CookBook documentation#One-section|documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">documentation</a>',
566 # link to the current page
576 # link to the current page
567 '[[Another page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Another page</a>',
577 '[[Another page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Another page</a>',
568 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
578 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
569 '[[Another page#anchor]]' => '<a href="#anchor" class="wiki-page">Another page</a>',
579 '[[Another page#anchor]]' => '<a href="#anchor" class="wiki-page">Another page</a>',
570 '[[Another page#anchor|Page]]' => '<a href="#anchor" class="wiki-page">Page</a>',
580 '[[Another page#anchor|Page]]' => '<a href="#anchor" class="wiki-page">Page</a>',
571 # page that doesn't exist
581 # page that doesn't exist
572 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
582 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
573 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">404</a>',
583 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">404</a>',
574 '[[Unknown page#anchor]]' => '<a href="/projects/ecookbook/wiki/Unknown_page#anchor" class="wiki-page new">Unknown page</a>',
584 '[[Unknown page#anchor]]' => '<a href="/projects/ecookbook/wiki/Unknown_page#anchor" class="wiki-page new">Unknown page</a>',
575 '[[Unknown page#anchor|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page#anchor" class="wiki-page new">404</a>',
585 '[[Unknown page#anchor|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page#anchor" class="wiki-page new">404</a>',
576 }
586 }
577
587
578 @project = Project.find(1)
588 @project = Project.find(1)
579
589
580 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(WikiContent.generate!( :text => text, :page => page ), :text) }
590 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(WikiContent.generate!( :text => text, :page => page ), :text) }
581 end
591 end
582
592
583 def test_table_of_content
593 def test_table_of_content
584 raw = <<-RAW
594 raw = <<-RAW
585 {{toc}}
595 {{toc}}
586
596
587 h1. Title
597 h1. Title
588
598
589 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
599 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
590
600
591 h2. Subtitle with a [[Wiki]] link
601 h2. Subtitle with a [[Wiki]] link
592
602
593 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
603 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
594
604
595 h2. Subtitle with [[Wiki|another Wiki]] link
605 h2. Subtitle with [[Wiki|another Wiki]] link
596
606
597 h2. Subtitle with %{color:red}red text%
607 h2. Subtitle with %{color:red}red text%
598
608
599 <pre>
609 <pre>
600 some code
610 some code
601 </pre>
611 </pre>
602
612
603 h3. Subtitle with *some* _modifiers_
613 h3. Subtitle with *some* _modifiers_
604
614
605 h1. Another title
615 h1. Another title
606
616
607 h3. An "Internet link":http://www.redmine.org/ inside subtitle
617 h3. An "Internet link":http://www.redmine.org/ inside subtitle
608
618
609 h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
619 h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
610
620
611 RAW
621 RAW
612
622
613 expected = '<ul class="toc">' +
623 expected = '<ul class="toc">' +
614 '<li><a href="#Title">Title</a>' +
624 '<li><a href="#Title">Title</a>' +
615 '<ul>' +
625 '<ul>' +
616 '<li><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
626 '<li><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
617 '<li><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
627 '<li><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
618 '<li><a href="#Subtitle-with-red-text">Subtitle with red text</a>' +
628 '<li><a href="#Subtitle-with-red-text">Subtitle with red text</a>' +
619 '<ul>' +
629 '<ul>' +
620 '<li><a href="#Subtitle-with-some-modifiers">Subtitle with some modifiers</a></li>' +
630 '<li><a href="#Subtitle-with-some-modifiers">Subtitle with some modifiers</a></li>' +
621 '</ul>' +
631 '</ul>' +
622 '</li>' +
632 '</li>' +
623 '</ul>' +
633 '</ul>' +
624 '</li>' +
634 '</li>' +
625 '<li><a href="#Another-title">Another title</a>' +
635 '<li><a href="#Another-title">Another title</a>' +
626 '<ul>' +
636 '<ul>' +
627 '<li>' +
637 '<li>' +
628 '<ul>' +
638 '<ul>' +
629 '<li><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
639 '<li><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
630 '</ul>' +
640 '</ul>' +
631 '</li>' +
641 '</li>' +
632 '<li><a href="#Project-Name">Project Name</a></li>' +
642 '<li><a href="#Project-Name">Project Name</a></li>' +
633 '</ul>' +
643 '</ul>' +
634 '</li>' +
644 '</li>' +
635 '</ul>'
645 '</ul>'
636
646
637 @project = Project.find(1)
647 @project = Project.find(1)
638 assert textilizable(raw).gsub("\n", "").include?(expected), textilizable(raw)
648 assert textilizable(raw).gsub("\n", "").include?(expected), textilizable(raw)
639 end
649 end
640
650
641 def test_table_of_content_should_contain_included_page_headings
651 def test_table_of_content_should_contain_included_page_headings
642 raw = <<-RAW
652 raw = <<-RAW
643 {{toc}}
653 {{toc}}
644
654
645 h1. Included
655 h1. Included
646
656
647 {{include(Child_1)}}
657 {{include(Child_1)}}
648 RAW
658 RAW
649
659
650 expected = '<ul class="toc">' +
660 expected = '<ul class="toc">' +
651 '<li><a href="#Included">Included</a></li>' +
661 '<li><a href="#Included">Included</a></li>' +
652 '<li><a href="#Child-page-1">Child page 1</a></li>' +
662 '<li><a href="#Child-page-1">Child page 1</a></li>' +
653 '</ul>'
663 '</ul>'
654
664
655 @project = Project.find(1)
665 @project = Project.find(1)
656 assert textilizable(raw).gsub("\n", "").include?(expected)
666 assert textilizable(raw).gsub("\n", "").include?(expected)
657 end
667 end
658
668
659 def test_default_formatter
669 def test_default_formatter
660 Setting.text_formatting = 'unknown'
670 Setting.text_formatting = 'unknown'
661 text = 'a *link*: http://www.example.net/'
671 text = 'a *link*: http://www.example.net/'
662 assert_equal '<p>a *link*: <a href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
672 assert_equal '<p>a *link*: <a href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
663 Setting.text_formatting = 'textile'
673 Setting.text_formatting = 'textile'
664 end
674 end
665
675
666 def test_due_date_distance_in_words
676 def test_due_date_distance_in_words
667 to_test = { Date.today => 'Due in 0 days',
677 to_test = { Date.today => 'Due in 0 days',
668 Date.today + 1 => 'Due in 1 day',
678 Date.today + 1 => 'Due in 1 day',
669 Date.today + 100 => 'Due in about 3 months',
679 Date.today + 100 => 'Due in about 3 months',
670 Date.today + 20000 => 'Due in over 54 years',
680 Date.today + 20000 => 'Due in over 54 years',
671 Date.today - 1 => '1 day late',
681 Date.today - 1 => '1 day late',
672 Date.today - 100 => 'about 3 months late',
682 Date.today - 100 => 'about 3 months late',
673 Date.today - 20000 => 'over 54 years late',
683 Date.today - 20000 => 'over 54 years late',
674 }
684 }
675 ::I18n.locale = :en
685 ::I18n.locale = :en
676 to_test.each do |date, expected|
686 to_test.each do |date, expected|
677 assert_equal expected, due_date_distance_in_words(date)
687 assert_equal expected, due_date_distance_in_words(date)
678 end
688 end
679 end
689 end
680
690
681 def test_avatar
691 def test_avatar
682 # turn on avatars
692 # turn on avatars
683 Setting.gravatar_enabled = '1'
693 Setting.gravatar_enabled = '1'
684 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
694 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
685 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
695 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
686 assert_nil avatar('jsmith')
696 assert_nil avatar('jsmith')
687 assert_nil avatar(nil)
697 assert_nil avatar(nil)
688
698
689 # turn off avatars
699 # turn off avatars
690 Setting.gravatar_enabled = '0'
700 Setting.gravatar_enabled = '0'
691 assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
701 assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
692 end
702 end
693
703
694 def test_link_to_user
704 def test_link_to_user
695 user = User.find(2)
705 user = User.find(2)
696 t = link_to_user(user)
706 t = link_to_user(user)
697 assert_equal "<a href=\"/users/2\">#{ user.name }</a>", t
707 assert_equal "<a href=\"/users/2\">#{ user.name }</a>", t
698 end
708 end
699
709
700 def test_link_to_user_should_not_link_to_locked_user
710 def test_link_to_user_should_not_link_to_locked_user
701 user = User.find(5)
711 user = User.find(5)
702 assert user.locked?
712 assert user.locked?
703 t = link_to_user(user)
713 t = link_to_user(user)
704 assert_equal user.name, t
714 assert_equal user.name, t
705 end
715 end
706
716
707 def test_link_to_user_should_not_link_to_anonymous
717 def test_link_to_user_should_not_link_to_anonymous
708 user = User.anonymous
718 user = User.anonymous
709 assert user.anonymous?
719 assert user.anonymous?
710 t = link_to_user(user)
720 t = link_to_user(user)
711 assert_equal ::I18n.t(:label_user_anonymous), t
721 assert_equal ::I18n.t(:label_user_anonymous), t
712 end
722 end
713
723
714 def test_link_to_project
724 def test_link_to_project
715 project = Project.find(1)
725 project = Project.find(1)
716 assert_equal %(<a href="/projects/ecookbook">eCookbook</a>),
726 assert_equal %(<a href="/projects/ecookbook">eCookbook</a>),
717 link_to_project(project)
727 link_to_project(project)
718 assert_equal %(<a href="/projects/ecookbook/settings">eCookbook</a>),
728 assert_equal %(<a href="/projects/ecookbook/settings">eCookbook</a>),
719 link_to_project(project, :action => 'settings')
729 link_to_project(project, :action => 'settings')
720 assert_equal %(<a href="http://test.host/projects/ecookbook?jump=blah">eCookbook</a>),
730 assert_equal %(<a href="http://test.host/projects/ecookbook?jump=blah">eCookbook</a>),
721 link_to_project(project, {:only_path => false, :jump => 'blah'})
731 link_to_project(project, {:only_path => false, :jump => 'blah'})
722 assert_equal %(<a href="/projects/ecookbook/settings" class="project">eCookbook</a>),
732 assert_equal %(<a href="/projects/ecookbook/settings" class="project">eCookbook</a>),
723 link_to_project(project, {:action => 'settings'}, :class => "project")
733 link_to_project(project, {:action => 'settings'}, :class => "project")
724 end
734 end
725 end
735 end
General Comments 0
You need to be logged in to leave comments. Login now