##// END OF EJS Templates
fix inconsistent image filename extensions (#9638)...
Toshi MARUYAMA -
r7771:9be9c5f56584
parent child
Show More
@@ -1,1052 +1,1052
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2011 Jean-Philippe Lang
4 # Copyright (C) 2006-2011 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 require 'forwardable'
20 require 'forwardable'
21 require 'cgi'
21 require 'cgi'
22
22
23 module ApplicationHelper
23 module ApplicationHelper
24 include Redmine::WikiFormatting::Macros::Definitions
24 include Redmine::WikiFormatting::Macros::Definitions
25 include Redmine::I18n
25 include Redmine::I18n
26 include GravatarHelper::PublicMethods
26 include GravatarHelper::PublicMethods
27
27
28 extend Forwardable
28 extend Forwardable
29 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
29 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
30
30
31 # Return true if user is authorized for controller/action, otherwise false
31 # Return true if user is authorized for controller/action, otherwise false
32 def authorize_for(controller, action)
32 def authorize_for(controller, action)
33 User.current.allowed_to?({:controller => controller, :action => action}, @project)
33 User.current.allowed_to?({:controller => controller, :action => action}, @project)
34 end
34 end
35
35
36 # Display a link if user is authorized
36 # Display a link if user is authorized
37 #
37 #
38 # @param [String] name Anchor text (passed to link_to)
38 # @param [String] name Anchor text (passed to link_to)
39 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
39 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
40 # @param [optional, Hash] html_options Options passed to link_to
40 # @param [optional, Hash] html_options Options passed to link_to
41 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
41 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
42 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
42 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
43 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
43 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
44 end
44 end
45
45
46 # Display a link to remote if user is authorized
46 # Display a link to remote if user is authorized
47 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
47 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
48 url = options[:url] || {}
48 url = options[:url] || {}
49 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
49 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
50 end
50 end
51
51
52 # Displays a link to user's account page if active
52 # Displays a link to user's account page if active
53 def link_to_user(user, options={})
53 def link_to_user(user, options={})
54 if user.is_a?(User)
54 if user.is_a?(User)
55 name = h(user.name(options[:format]))
55 name = h(user.name(options[:format]))
56 if user.active?
56 if user.active?
57 link_to name, :controller => 'users', :action => 'show', :id => user
57 link_to name, :controller => 'users', :action => 'show', :id => user
58 else
58 else
59 name
59 name
60 end
60 end
61 else
61 else
62 h(user.to_s)
62 h(user.to_s)
63 end
63 end
64 end
64 end
65
65
66 # Displays a link to +issue+ with its subject.
66 # Displays a link to +issue+ with its subject.
67 # Examples:
67 # Examples:
68 #
68 #
69 # link_to_issue(issue) # => Defect #6: This is the subject
69 # link_to_issue(issue) # => Defect #6: This is the subject
70 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
70 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
71 # link_to_issue(issue, :subject => false) # => Defect #6
71 # link_to_issue(issue, :subject => false) # => Defect #6
72 # link_to_issue(issue, :project => true) # => Foo - Defect #6
72 # link_to_issue(issue, :project => true) # => Foo - Defect #6
73 #
73 #
74 def link_to_issue(issue, options={})
74 def link_to_issue(issue, options={})
75 title = nil
75 title = nil
76 subject = nil
76 subject = nil
77 if options[:subject] == false
77 if options[:subject] == false
78 title = truncate(issue.subject, :length => 60)
78 title = truncate(issue.subject, :length => 60)
79 else
79 else
80 subject = issue.subject
80 subject = issue.subject
81 if options[:truncate]
81 if options[:truncate]
82 subject = truncate(subject, :length => options[:truncate])
82 subject = truncate(subject, :length => options[:truncate])
83 end
83 end
84 end
84 end
85 s = link_to "#{h(issue.tracker)} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
85 s = link_to "#{h(issue.tracker)} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
86 :class => issue.css_classes,
86 :class => issue.css_classes,
87 :title => title
87 :title => title
88 s << ": #{h subject}" if subject
88 s << ": #{h subject}" if subject
89 s = "#{h issue.project} - " + s if options[:project]
89 s = "#{h issue.project} - " + s if options[:project]
90 s
90 s
91 end
91 end
92
92
93 # Generates a link to an attachment.
93 # Generates a link to an attachment.
94 # Options:
94 # Options:
95 # * :text - Link text (default to attachment filename)
95 # * :text - Link text (default to attachment filename)
96 # * :download - Force download (default: false)
96 # * :download - Force download (default: false)
97 def link_to_attachment(attachment, options={})
97 def link_to_attachment(attachment, options={})
98 text = options.delete(:text) || attachment.filename
98 text = options.delete(:text) || attachment.filename
99 action = options.delete(:download) ? 'download' : 'show'
99 action = options.delete(:download) ? 'download' : 'show'
100 link_to(h(text),
100 link_to(h(text),
101 {:controller => 'attachments', :action => action,
101 {:controller => 'attachments', :action => action,
102 :id => attachment, :filename => attachment.filename },
102 :id => attachment, :filename => attachment.filename },
103 options)
103 options)
104 end
104 end
105
105
106 # Generates a link to a SCM revision
106 # Generates a link to a SCM revision
107 # Options:
107 # Options:
108 # * :text - Link text (default to the formatted revision)
108 # * :text - Link text (default to the formatted revision)
109 def link_to_revision(revision, project, options={})
109 def link_to_revision(revision, project, options={})
110 text = options.delete(:text) || format_revision(revision)
110 text = options.delete(:text) || format_revision(revision)
111 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
111 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
112
112
113 link_to(h(text), {:controller => 'repositories', :action => 'revision', :id => project, :rev => rev},
113 link_to(h(text), {:controller => 'repositories', :action => 'revision', :id => project, :rev => rev},
114 :title => l(:label_revision_id, format_revision(revision)))
114 :title => l(:label_revision_id, format_revision(revision)))
115 end
115 end
116
116
117 # Generates a link to a message
117 # Generates a link to a message
118 def link_to_message(message, options={}, html_options = nil)
118 def link_to_message(message, options={}, html_options = nil)
119 link_to(
119 link_to(
120 h(truncate(message.subject, :length => 60)),
120 h(truncate(message.subject, :length => 60)),
121 { :controller => 'messages', :action => 'show',
121 { :controller => 'messages', :action => 'show',
122 :board_id => message.board_id,
122 :board_id => message.board_id,
123 :id => message.root,
123 :id => message.root,
124 :r => (message.parent_id && message.id),
124 :r => (message.parent_id && message.id),
125 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
125 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
126 }.merge(options),
126 }.merge(options),
127 html_options
127 html_options
128 )
128 )
129 end
129 end
130
130
131 # Generates a link to a project if active
131 # Generates a link to a project if active
132 # Examples:
132 # Examples:
133 #
133 #
134 # link_to_project(project) # => link to the specified project overview
134 # link_to_project(project) # => link to the specified project overview
135 # link_to_project(project, :action=>'settings') # => link to project settings
135 # link_to_project(project, :action=>'settings') # => link to project settings
136 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
136 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
137 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
137 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
138 #
138 #
139 def link_to_project(project, options={}, html_options = nil)
139 def link_to_project(project, options={}, html_options = nil)
140 if project.active?
140 if project.active?
141 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
141 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
142 link_to(h(project), url, html_options)
142 link_to(h(project), url, html_options)
143 else
143 else
144 h(project)
144 h(project)
145 end
145 end
146 end
146 end
147
147
148 def toggle_link(name, id, options={})
148 def toggle_link(name, id, options={})
149 onclick = "Element.toggle('#{id}'); "
149 onclick = "Element.toggle('#{id}'); "
150 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
150 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
151 onclick << "return false;"
151 onclick << "return false;"
152 link_to(name, "#", :onclick => onclick)
152 link_to(name, "#", :onclick => onclick)
153 end
153 end
154
154
155 def image_to_function(name, function, html_options = {})
155 def image_to_function(name, function, html_options = {})
156 html_options.symbolize_keys!
156 html_options.symbolize_keys!
157 tag(:input, html_options.merge({
157 tag(:input, html_options.merge({
158 :type => "image", :src => image_path(name),
158 :type => "image", :src => image_path(name),
159 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
159 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
160 }))
160 }))
161 end
161 end
162
162
163 def prompt_to_remote(name, text, param, url, html_options = {})
163 def prompt_to_remote(name, text, param, url, html_options = {})
164 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
164 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
165 link_to name, {}, html_options
165 link_to name, {}, html_options
166 end
166 end
167
167
168 def format_activity_title(text)
168 def format_activity_title(text)
169 h(truncate_single_line(text, :length => 100))
169 h(truncate_single_line(text, :length => 100))
170 end
170 end
171
171
172 def format_activity_day(date)
172 def format_activity_day(date)
173 date == Date.today ? l(:label_today).titleize : format_date(date)
173 date == Date.today ? l(:label_today).titleize : format_date(date)
174 end
174 end
175
175
176 def format_activity_description(text)
176 def format_activity_description(text)
177 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
177 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
178 end
178 end
179
179
180 def format_version_name(version)
180 def format_version_name(version)
181 if version.project == @project
181 if version.project == @project
182 h(version)
182 h(version)
183 else
183 else
184 h("#{version.project} - #{version}")
184 h("#{version.project} - #{version}")
185 end
185 end
186 end
186 end
187
187
188 def due_date_distance_in_words(date)
188 def due_date_distance_in_words(date)
189 if date
189 if date
190 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
190 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
191 end
191 end
192 end
192 end
193
193
194 def render_page_hierarchy(pages, node=nil, options={})
194 def render_page_hierarchy(pages, node=nil, options={})
195 content = ''
195 content = ''
196 if pages[node]
196 if pages[node]
197 content << "<ul class=\"pages-hierarchy\">\n"
197 content << "<ul class=\"pages-hierarchy\">\n"
198 pages[node].each do |page|
198 pages[node].each do |page|
199 content << "<li>"
199 content << "<li>"
200 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
200 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
201 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
201 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
202 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
202 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
203 content << "</li>\n"
203 content << "</li>\n"
204 end
204 end
205 content << "</ul>\n"
205 content << "</ul>\n"
206 end
206 end
207 content.html_safe
207 content.html_safe
208 end
208 end
209
209
210 # Renders flash messages
210 # Renders flash messages
211 def render_flash_messages
211 def render_flash_messages
212 s = ''
212 s = ''
213 flash.each do |k,v|
213 flash.each do |k,v|
214 s << content_tag('div', v, :class => "flash #{k}")
214 s << content_tag('div', v, :class => "flash #{k}")
215 end
215 end
216 s.html_safe
216 s.html_safe
217 end
217 end
218
218
219 # Renders tabs and their content
219 # Renders tabs and their content
220 def render_tabs(tabs)
220 def render_tabs(tabs)
221 if tabs.any?
221 if tabs.any?
222 render :partial => 'common/tabs', :locals => {:tabs => tabs}
222 render :partial => 'common/tabs', :locals => {:tabs => tabs}
223 else
223 else
224 content_tag 'p', l(:label_no_data), :class => "nodata"
224 content_tag 'p', l(:label_no_data), :class => "nodata"
225 end
225 end
226 end
226 end
227
227
228 # Renders the project quick-jump box
228 # Renders the project quick-jump box
229 def render_project_jump_box
229 def render_project_jump_box
230 return unless User.current.logged?
230 return unless User.current.logged?
231 projects = User.current.memberships.collect(&:project).compact.uniq
231 projects = User.current.memberships.collect(&:project).compact.uniq
232 if projects.any?
232 if projects.any?
233 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
233 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
234 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
234 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
235 '<option value="" disabled="disabled">---</option>'
235 '<option value="" disabled="disabled">---</option>'
236 s << project_tree_options_for_select(projects, :selected => @project) do |p|
236 s << project_tree_options_for_select(projects, :selected => @project) do |p|
237 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
237 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
238 end
238 end
239 s << '</select>'
239 s << '</select>'
240 s.html_safe
240 s.html_safe
241 end
241 end
242 end
242 end
243
243
244 def project_tree_options_for_select(projects, options = {})
244 def project_tree_options_for_select(projects, options = {})
245 s = ''
245 s = ''
246 project_tree(projects) do |project, level|
246 project_tree(projects) do |project, level|
247 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
247 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
248 tag_options = {:value => project.id}
248 tag_options = {:value => project.id}
249 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
249 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
250 tag_options[:selected] = 'selected'
250 tag_options[:selected] = 'selected'
251 else
251 else
252 tag_options[:selected] = nil
252 tag_options[:selected] = nil
253 end
253 end
254 tag_options.merge!(yield(project)) if block_given?
254 tag_options.merge!(yield(project)) if block_given?
255 s << content_tag('option', name_prefix + h(project), tag_options)
255 s << content_tag('option', name_prefix + h(project), tag_options)
256 end
256 end
257 s.html_safe
257 s.html_safe
258 end
258 end
259
259
260 # Yields the given block for each project with its level in the tree
260 # Yields the given block for each project with its level in the tree
261 #
261 #
262 # Wrapper for Project#project_tree
262 # Wrapper for Project#project_tree
263 def project_tree(projects, &block)
263 def project_tree(projects, &block)
264 Project.project_tree(projects, &block)
264 Project.project_tree(projects, &block)
265 end
265 end
266
266
267 def project_nested_ul(projects, &block)
267 def project_nested_ul(projects, &block)
268 s = ''
268 s = ''
269 if projects.any?
269 if projects.any?
270 ancestors = []
270 ancestors = []
271 projects.sort_by(&:lft).each do |project|
271 projects.sort_by(&:lft).each do |project|
272 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
272 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
273 s << "<ul>\n"
273 s << "<ul>\n"
274 else
274 else
275 ancestors.pop
275 ancestors.pop
276 s << "</li>"
276 s << "</li>"
277 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
277 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
278 ancestors.pop
278 ancestors.pop
279 s << "</ul></li>\n"
279 s << "</ul></li>\n"
280 end
280 end
281 end
281 end
282 s << "<li>"
282 s << "<li>"
283 s << yield(project).to_s
283 s << yield(project).to_s
284 ancestors << project
284 ancestors << project
285 end
285 end
286 s << ("</li></ul>\n" * ancestors.size)
286 s << ("</li></ul>\n" * ancestors.size)
287 end
287 end
288 s.html_safe
288 s.html_safe
289 end
289 end
290
290
291 def principals_check_box_tags(name, principals)
291 def principals_check_box_tags(name, principals)
292 s = ''
292 s = ''
293 principals.sort.each do |principal|
293 principals.sort.each do |principal|
294 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
294 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
295 end
295 end
296 s.html_safe
296 s.html_safe
297 end
297 end
298
298
299 # Returns a string for users/groups option tags
299 # Returns a string for users/groups option tags
300 def principals_options_for_select(collection, selected=nil)
300 def principals_options_for_select(collection, selected=nil)
301 s = ''
301 s = ''
302 groups = ''
302 groups = ''
303 collection.sort.each do |element|
303 collection.sort.each do |element|
304 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected)
304 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected)
305 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
305 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
306 end
306 end
307 unless groups.empty?
307 unless groups.empty?
308 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
308 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
309 end
309 end
310 s
310 s
311 end
311 end
312
312
313 # Truncates and returns the string as a single line
313 # Truncates and returns the string as a single line
314 def truncate_single_line(string, *args)
314 def truncate_single_line(string, *args)
315 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
315 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
316 end
316 end
317
317
318 # Truncates at line break after 250 characters or options[:length]
318 # Truncates at line break after 250 characters or options[:length]
319 def truncate_lines(string, options={})
319 def truncate_lines(string, options={})
320 length = options[:length] || 250
320 length = options[:length] || 250
321 if string.to_s =~ /\A(.{#{length}}.*?)$/m
321 if string.to_s =~ /\A(.{#{length}}.*?)$/m
322 "#{$1}..."
322 "#{$1}..."
323 else
323 else
324 string
324 string
325 end
325 end
326 end
326 end
327
327
328 def html_hours(text)
328 def html_hours(text)
329 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
329 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
330 end
330 end
331
331
332 def authoring(created, author, options={})
332 def authoring(created, author, options={})
333 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
333 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
334 end
334 end
335
335
336 def time_tag(time)
336 def time_tag(time)
337 text = distance_of_time_in_words(Time.now, time)
337 text = distance_of_time_in_words(Time.now, time)
338 if @project
338 if @project
339 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
339 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
340 else
340 else
341 content_tag('acronym', text, :title => format_time(time))
341 content_tag('acronym', text, :title => format_time(time))
342 end
342 end
343 end
343 end
344
344
345 def syntax_highlight(name, content)
345 def syntax_highlight(name, content)
346 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
346 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
347 end
347 end
348
348
349 def to_path_param(path)
349 def to_path_param(path)
350 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
350 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
351 end
351 end
352
352
353 def pagination_links_full(paginator, count=nil, options={})
353 def pagination_links_full(paginator, count=nil, options={})
354 page_param = options.delete(:page_param) || :page
354 page_param = options.delete(:page_param) || :page
355 per_page_links = options.delete(:per_page_links)
355 per_page_links = options.delete(:per_page_links)
356 url_param = params.dup
356 url_param = params.dup
357
357
358 html = ''
358 html = ''
359 if paginator.current.previous
359 if paginator.current.previous
360 # \xc2\xab(utf-8) = &#171;
360 # \xc2\xab(utf-8) = &#171;
361 html << link_to_content_update(
361 html << link_to_content_update(
362 "\xc2\xab " + l(:label_previous),
362 "\xc2\xab " + l(:label_previous),
363 url_param.merge(page_param => paginator.current.previous)) + ' '
363 url_param.merge(page_param => paginator.current.previous)) + ' '
364 end
364 end
365
365
366 html << (pagination_links_each(paginator, options) do |n|
366 html << (pagination_links_each(paginator, options) do |n|
367 link_to_content_update(n.to_s, url_param.merge(page_param => n))
367 link_to_content_update(n.to_s, url_param.merge(page_param => n))
368 end || '')
368 end || '')
369
369
370 if paginator.current.next
370 if paginator.current.next
371 # \xc2\xbb(utf-8) = &#187;
371 # \xc2\xbb(utf-8) = &#187;
372 html << ' ' + link_to_content_update(
372 html << ' ' + link_to_content_update(
373 (l(:label_next) + " \xc2\xbb"),
373 (l(:label_next) + " \xc2\xbb"),
374 url_param.merge(page_param => paginator.current.next))
374 url_param.merge(page_param => paginator.current.next))
375 end
375 end
376
376
377 unless count.nil?
377 unless count.nil?
378 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
378 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
379 if per_page_links != false && links = per_page_links(paginator.items_per_page)
379 if per_page_links != false && links = per_page_links(paginator.items_per_page)
380 html << " | #{links}"
380 html << " | #{links}"
381 end
381 end
382 end
382 end
383
383
384 html.html_safe
384 html.html_safe
385 end
385 end
386
386
387 def per_page_links(selected=nil)
387 def per_page_links(selected=nil)
388 links = Setting.per_page_options_array.collect do |n|
388 links = Setting.per_page_options_array.collect do |n|
389 n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
389 n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
390 end
390 end
391 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
391 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
392 end
392 end
393
393
394 def reorder_links(name, url, method = :post)
394 def reorder_links(name, url, method = :post)
395 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
395 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
396 url.merge({"#{name}[move_to]" => 'highest'}),
396 url.merge({"#{name}[move_to]" => 'highest'}),
397 :method => method, :title => l(:label_sort_highest)) +
397 :method => method, :title => l(:label_sort_highest)) +
398 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
398 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
399 url.merge({"#{name}[move_to]" => 'higher'}),
399 url.merge({"#{name}[move_to]" => 'higher'}),
400 :method => method, :title => l(:label_sort_higher)) +
400 :method => method, :title => l(:label_sort_higher)) +
401 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
401 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
402 url.merge({"#{name}[move_to]" => 'lower'}),
402 url.merge({"#{name}[move_to]" => 'lower'}),
403 :method => method, :title => l(:label_sort_lower)) +
403 :method => method, :title => l(:label_sort_lower)) +
404 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
404 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
405 url.merge({"#{name}[move_to]" => 'lowest'}),
405 url.merge({"#{name}[move_to]" => 'lowest'}),
406 :method => method, :title => l(:label_sort_lowest))
406 :method => method, :title => l(:label_sort_lowest))
407 end
407 end
408
408
409 def breadcrumb(*args)
409 def breadcrumb(*args)
410 elements = args.flatten
410 elements = args.flatten
411 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
411 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
412 end
412 end
413
413
414 def other_formats_links(&block)
414 def other_formats_links(&block)
415 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
415 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
416 yield Redmine::Views::OtherFormatsBuilder.new(self)
416 yield Redmine::Views::OtherFormatsBuilder.new(self)
417 concat('</p>'.html_safe)
417 concat('</p>'.html_safe)
418 end
418 end
419
419
420 def page_header_title
420 def page_header_title
421 if @project.nil? || @project.new_record?
421 if @project.nil? || @project.new_record?
422 h(Setting.app_title)
422 h(Setting.app_title)
423 else
423 else
424 b = []
424 b = []
425 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
425 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
426 if ancestors.any?
426 if ancestors.any?
427 root = ancestors.shift
427 root = ancestors.shift
428 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
428 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
429 if ancestors.size > 2
429 if ancestors.size > 2
430 b << "\xe2\x80\xa6"
430 b << "\xe2\x80\xa6"
431 ancestors = ancestors[-2, 2]
431 ancestors = ancestors[-2, 2]
432 end
432 end
433 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
433 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
434 end
434 end
435 b << h(@project)
435 b << h(@project)
436 b.join(" \xc2\xbb ").html_safe
436 b.join(" \xc2\xbb ").html_safe
437 end
437 end
438 end
438 end
439
439
440 def html_title(*args)
440 def html_title(*args)
441 if args.empty?
441 if args.empty?
442 title = @html_title || []
442 title = @html_title || []
443 title << @project.name if @project
443 title << @project.name if @project
444 title << Setting.app_title unless Setting.app_title == title.last
444 title << Setting.app_title unless Setting.app_title == title.last
445 title.select {|t| !t.blank? }.join(' - ')
445 title.select {|t| !t.blank? }.join(' - ')
446 else
446 else
447 @html_title ||= []
447 @html_title ||= []
448 @html_title += args
448 @html_title += args
449 end
449 end
450 end
450 end
451
451
452 # Returns the theme, controller name, and action as css classes for the
452 # Returns the theme, controller name, and action as css classes for the
453 # HTML body.
453 # HTML body.
454 def body_css_classes
454 def body_css_classes
455 css = []
455 css = []
456 if theme = Redmine::Themes.theme(Setting.ui_theme)
456 if theme = Redmine::Themes.theme(Setting.ui_theme)
457 css << 'theme-' + theme.name
457 css << 'theme-' + theme.name
458 end
458 end
459
459
460 css << 'controller-' + params[:controller]
460 css << 'controller-' + params[:controller]
461 css << 'action-' + params[:action]
461 css << 'action-' + params[:action]
462 css.join(' ')
462 css.join(' ')
463 end
463 end
464
464
465 def accesskey(s)
465 def accesskey(s)
466 Redmine::AccessKeys.key_for s
466 Redmine::AccessKeys.key_for s
467 end
467 end
468
468
469 # Formats text according to system settings.
469 # Formats text according to system settings.
470 # 2 ways to call this method:
470 # 2 ways to call this method:
471 # * with a String: textilizable(text, options)
471 # * with a String: textilizable(text, options)
472 # * with an object and one of its attribute: textilizable(issue, :description, options)
472 # * with an object and one of its attribute: textilizable(issue, :description, options)
473 def textilizable(*args)
473 def textilizable(*args)
474 options = args.last.is_a?(Hash) ? args.pop : {}
474 options = args.last.is_a?(Hash) ? args.pop : {}
475 case args.size
475 case args.size
476 when 1
476 when 1
477 obj = options[:object]
477 obj = options[:object]
478 text = args.shift
478 text = args.shift
479 when 2
479 when 2
480 obj = args.shift
480 obj = args.shift
481 attr = args.shift
481 attr = args.shift
482 text = obj.send(attr).to_s
482 text = obj.send(attr).to_s
483 else
483 else
484 raise ArgumentError, 'invalid arguments to textilizable'
484 raise ArgumentError, 'invalid arguments to textilizable'
485 end
485 end
486 return '' if text.blank?
486 return '' if text.blank?
487 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
487 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
488 only_path = options.delete(:only_path) == false ? false : true
488 only_path = options.delete(:only_path) == false ? false : true
489
489
490 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
490 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
491
491
492 @parsed_headings = []
492 @parsed_headings = []
493 @current_section = 0 if options[:edit_section_links]
493 @current_section = 0 if options[:edit_section_links]
494 text = parse_non_pre_blocks(text) do |text|
494 text = parse_non_pre_blocks(text) do |text|
495 [:parse_sections, :parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_macros, :parse_headings].each do |method_name|
495 [:parse_sections, :parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_macros, :parse_headings].each do |method_name|
496 send method_name, text, project, obj, attr, only_path, options
496 send method_name, text, project, obj, attr, only_path, options
497 end
497 end
498 end
498 end
499
499
500 if @parsed_headings.any?
500 if @parsed_headings.any?
501 replace_toc(text, @parsed_headings)
501 replace_toc(text, @parsed_headings)
502 end
502 end
503
503
504 text
504 text
505 end
505 end
506
506
507 def parse_non_pre_blocks(text)
507 def parse_non_pre_blocks(text)
508 s = StringScanner.new(text)
508 s = StringScanner.new(text)
509 tags = []
509 tags = []
510 parsed = ''
510 parsed = ''
511 while !s.eos?
511 while !s.eos?
512 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
512 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
513 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
513 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
514 if tags.empty?
514 if tags.empty?
515 yield text
515 yield text
516 end
516 end
517 parsed << text
517 parsed << text
518 if tag
518 if tag
519 if closing
519 if closing
520 if tags.last == tag.downcase
520 if tags.last == tag.downcase
521 tags.pop
521 tags.pop
522 end
522 end
523 else
523 else
524 tags << tag.downcase
524 tags << tag.downcase
525 end
525 end
526 parsed << full_tag
526 parsed << full_tag
527 end
527 end
528 end
528 end
529 # Close any non closing tags
529 # Close any non closing tags
530 while tag = tags.pop
530 while tag = tags.pop
531 parsed << "</#{tag}>"
531 parsed << "</#{tag}>"
532 end
532 end
533 parsed.html_safe
533 parsed.html_safe
534 end
534 end
535
535
536 def parse_inline_attachments(text, project, obj, attr, only_path, options)
536 def parse_inline_attachments(text, project, obj, attr, only_path, options)
537 # when using an image link, try to use an attachment, if possible
537 # when using an image link, try to use an attachment, if possible
538 if options[:attachments] || (obj && obj.respond_to?(:attachments))
538 if options[:attachments] || (obj && obj.respond_to?(:attachments))
539 attachments = nil
539 attachments = nil
540 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
540 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
541 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
541 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
542 attachments ||= (options[:attachments] || obj.attachments).sort_by(&:created_on).reverse
542 attachments ||= (options[:attachments] || obj.attachments).sort_by(&:created_on).reverse
543 # search for the picture in attachments
543 # search for the picture in attachments
544 if found = attachments.detect { |att| att.filename.downcase == filename }
544 if found = attachments.detect { |att| att.filename.downcase == filename }
545 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
545 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
546 desc = found.description.to_s.gsub('"', '')
546 desc = found.description.to_s.gsub('"', '')
547 if !desc.blank? && alttext.blank?
547 if !desc.blank? && alttext.blank?
548 alt = " title=\"#{desc}\" alt=\"#{desc}\""
548 alt = " title=\"#{desc}\" alt=\"#{desc}\""
549 end
549 end
550 "src=\"#{image_url}\"#{alt}".html_safe
550 "src=\"#{image_url}\"#{alt}".html_safe
551 else
551 else
552 m.html_safe
552 m.html_safe
553 end
553 end
554 end
554 end
555 end
555 end
556 end
556 end
557
557
558 # Wiki links
558 # Wiki links
559 #
559 #
560 # Examples:
560 # Examples:
561 # [[mypage]]
561 # [[mypage]]
562 # [[mypage|mytext]]
562 # [[mypage|mytext]]
563 # wiki links can refer other project wikis, using project name or identifier:
563 # wiki links can refer other project wikis, using project name or identifier:
564 # [[project:]] -> wiki starting page
564 # [[project:]] -> wiki starting page
565 # [[project:|mytext]]
565 # [[project:|mytext]]
566 # [[project:mypage]]
566 # [[project:mypage]]
567 # [[project:mypage|mytext]]
567 # [[project:mypage|mytext]]
568 def parse_wiki_links(text, project, obj, attr, only_path, options)
568 def parse_wiki_links(text, project, obj, attr, only_path, options)
569 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
569 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
570 link_project = project
570 link_project = project
571 esc, all, page, title = $1, $2, $3, $5
571 esc, all, page, title = $1, $2, $3, $5
572 if esc.nil?
572 if esc.nil?
573 if page =~ /^([^\:]+)\:(.*)$/
573 if page =~ /^([^\:]+)\:(.*)$/
574 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
574 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
575 page = $2
575 page = $2
576 title ||= $1 if page.blank?
576 title ||= $1 if page.blank?
577 end
577 end
578
578
579 if link_project && link_project.wiki
579 if link_project && link_project.wiki
580 # extract anchor
580 # extract anchor
581 anchor = nil
581 anchor = nil
582 if page =~ /^(.+?)\#(.+)$/
582 if page =~ /^(.+?)\#(.+)$/
583 page, anchor = $1, $2
583 page, anchor = $1, $2
584 end
584 end
585 anchor = sanitize_anchor_name(anchor) if anchor.present?
585 anchor = sanitize_anchor_name(anchor) if anchor.present?
586 # check if page exists
586 # check if page exists
587 wiki_page = link_project.wiki.find_page(page)
587 wiki_page = link_project.wiki.find_page(page)
588 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
588 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
589 "##{anchor}"
589 "##{anchor}"
590 else
590 else
591 case options[:wiki_links]
591 case options[:wiki_links]
592 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
592 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
593 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
593 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
594 else
594 else
595 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
595 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
596 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, :id => wiki_page_id, :anchor => anchor)
596 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, :id => wiki_page_id, :anchor => anchor)
597 end
597 end
598 end
598 end
599 link_to(title || h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
599 link_to(title || h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
600 else
600 else
601 # project or wiki doesn't exist
601 # project or wiki doesn't exist
602 all.html_safe
602 all.html_safe
603 end
603 end
604 else
604 else
605 all.html_safe
605 all.html_safe
606 end
606 end
607 end
607 end
608 end
608 end
609
609
610 # Redmine links
610 # Redmine links
611 #
611 #
612 # Examples:
612 # Examples:
613 # Issues:
613 # Issues:
614 # #52 -> Link to issue #52
614 # #52 -> Link to issue #52
615 # Changesets:
615 # Changesets:
616 # r52 -> Link to revision 52
616 # r52 -> Link to revision 52
617 # commit:a85130f -> Link to scmid starting with a85130f
617 # commit:a85130f -> Link to scmid starting with a85130f
618 # Documents:
618 # Documents:
619 # document#17 -> Link to document with id 17
619 # document#17 -> Link to document with id 17
620 # document:Greetings -> Link to the document with title "Greetings"
620 # document:Greetings -> Link to the document with title "Greetings"
621 # document:"Some document" -> Link to the document with title "Some document"
621 # document:"Some document" -> Link to the document with title "Some document"
622 # Versions:
622 # Versions:
623 # version#3 -> Link to version with id 3
623 # version#3 -> Link to version with id 3
624 # version:1.0.0 -> Link to version named "1.0.0"
624 # version:1.0.0 -> Link to version named "1.0.0"
625 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
625 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
626 # Attachments:
626 # Attachments:
627 # attachment:file.zip -> Link to the attachment of the current object named file.zip
627 # attachment:file.zip -> Link to the attachment of the current object named file.zip
628 # Source files:
628 # Source files:
629 # source:some/file -> Link to the file located at /some/file in the project's repository
629 # source:some/file -> Link to the file located at /some/file in the project's repository
630 # source:some/file@52 -> Link to the file's revision 52
630 # source:some/file@52 -> Link to the file's revision 52
631 # source:some/file#L120 -> Link to line 120 of the file
631 # source:some/file#L120 -> Link to line 120 of the file
632 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
632 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
633 # export:some/file -> Force the download of the file
633 # export:some/file -> Force the download of the file
634 # Forum messages:
634 # Forum messages:
635 # message#1218 -> Link to message with id 1218
635 # message#1218 -> Link to message with id 1218
636 #
636 #
637 # Links can refer other objects from other projects, using project identifier:
637 # Links can refer other objects from other projects, using project identifier:
638 # identifier:r52
638 # identifier:r52
639 # identifier:document:"Some document"
639 # identifier:document:"Some document"
640 # identifier:version:1.0.0
640 # identifier:version:1.0.0
641 # identifier:source:some/file
641 # identifier:source:some/file
642 def parse_redmine_links(text, project, obj, attr, only_path, options)
642 def parse_redmine_links(text, project, obj, attr, only_path, options)
643 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-]+):)?(attachment|document|version|forum|news|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m|
643 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-]+):)?(attachment|document|version|forum|news|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m|
644 leading, esc, project_prefix, project_identifier, prefix, sep, identifier = $1, $2, $3, $4, $5, $7 || $9, $8 || $10
644 leading, esc, project_prefix, project_identifier, prefix, sep, identifier = $1, $2, $3, $4, $5, $7 || $9, $8 || $10
645 link = nil
645 link = nil
646 if project_identifier
646 if project_identifier
647 project = Project.visible.find_by_identifier(project_identifier)
647 project = Project.visible.find_by_identifier(project_identifier)
648 end
648 end
649 if esc.nil?
649 if esc.nil?
650 if prefix.nil? && sep == 'r'
650 if prefix.nil? && sep == 'r'
651 # project.changesets.visible raises an SQL error because of a double join on repositories
651 # project.changesets.visible raises an SQL error because of a double join on repositories
652 if project && project.repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(project.repository.id, identifier))
652 if project && project.repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(project.repository.id, identifier))
653 link = link_to(h("#{project_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
653 link = link_to(h("#{project_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
654 :class => 'changeset',
654 :class => 'changeset',
655 :title => truncate_single_line(changeset.comments, :length => 100))
655 :title => truncate_single_line(changeset.comments, :length => 100))
656 end
656 end
657 elsif sep == '#'
657 elsif sep == '#'
658 oid = identifier.to_i
658 oid = identifier.to_i
659 case prefix
659 case prefix
660 when nil
660 when nil
661 if issue = Issue.visible.find_by_id(oid, :include => :status)
661 if issue = Issue.visible.find_by_id(oid, :include => :status)
662 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
662 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
663 :class => issue.css_classes,
663 :class => issue.css_classes,
664 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
664 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
665 end
665 end
666 when 'document'
666 when 'document'
667 if document = Document.visible.find_by_id(oid)
667 if document = Document.visible.find_by_id(oid)
668 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
668 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
669 :class => 'document'
669 :class => 'document'
670 end
670 end
671 when 'version'
671 when 'version'
672 if version = Version.visible.find_by_id(oid)
672 if version = Version.visible.find_by_id(oid)
673 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
673 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
674 :class => 'version'
674 :class => 'version'
675 end
675 end
676 when 'message'
676 when 'message'
677 if message = Message.visible.find_by_id(oid, :include => :parent)
677 if message = Message.visible.find_by_id(oid, :include => :parent)
678 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
678 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
679 end
679 end
680 when 'forum'
680 when 'forum'
681 if board = Board.visible.find_by_id(oid)
681 if board = Board.visible.find_by_id(oid)
682 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
682 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
683 :class => 'board'
683 :class => 'board'
684 end
684 end
685 when 'news'
685 when 'news'
686 if news = News.visible.find_by_id(oid)
686 if news = News.visible.find_by_id(oid)
687 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
687 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
688 :class => 'news'
688 :class => 'news'
689 end
689 end
690 when 'project'
690 when 'project'
691 if p = Project.visible.find_by_id(oid)
691 if p = Project.visible.find_by_id(oid)
692 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
692 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
693 end
693 end
694 end
694 end
695 elsif sep == ':'
695 elsif sep == ':'
696 # removes the double quotes if any
696 # removes the double quotes if any
697 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
697 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
698 case prefix
698 case prefix
699 when 'document'
699 when 'document'
700 if project && document = project.documents.visible.find_by_title(name)
700 if project && document = project.documents.visible.find_by_title(name)
701 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
701 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
702 :class => 'document'
702 :class => 'document'
703 end
703 end
704 when 'version'
704 when 'version'
705 if project && version = project.versions.visible.find_by_name(name)
705 if project && version = project.versions.visible.find_by_name(name)
706 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
706 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
707 :class => 'version'
707 :class => 'version'
708 end
708 end
709 when 'forum'
709 when 'forum'
710 if project && board = project.boards.visible.find_by_name(name)
710 if project && board = project.boards.visible.find_by_name(name)
711 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
711 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
712 :class => 'board'
712 :class => 'board'
713 end
713 end
714 when 'news'
714 when 'news'
715 if project && news = project.news.visible.find_by_title(name)
715 if project && news = project.news.visible.find_by_title(name)
716 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
716 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
717 :class => 'news'
717 :class => 'news'
718 end
718 end
719 when 'commit'
719 when 'commit'
720 if project && project.repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", project.repository.id, "#{name}%"]))
720 if project && project.repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", project.repository.id, "#{name}%"]))
721 link = link_to h("#{project_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.identifier},
721 link = link_to h("#{project_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.identifier},
722 :class => 'changeset',
722 :class => 'changeset',
723 :title => truncate_single_line(h(changeset.comments), :length => 100)
723 :title => truncate_single_line(h(changeset.comments), :length => 100)
724 end
724 end
725 when 'source', 'export'
725 when 'source', 'export'
726 if project && project.repository && User.current.allowed_to?(:browse_repository, project)
726 if project && project.repository && User.current.allowed_to?(:browse_repository, project)
727 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
727 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
728 path, rev, anchor = $1, $3, $5
728 path, rev, anchor = $1, $3, $5
729 link = link_to h("#{project_prefix}#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
729 link = link_to h("#{project_prefix}#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
730 :path => to_path_param(path),
730 :path => to_path_param(path),
731 :rev => rev,
731 :rev => rev,
732 :anchor => anchor,
732 :anchor => anchor,
733 :format => (prefix == 'export' ? 'raw' : nil)},
733 :format => (prefix == 'export' ? 'raw' : nil)},
734 :class => (prefix == 'export' ? 'source download' : 'source')
734 :class => (prefix == 'export' ? 'source download' : 'source')
735 end
735 end
736 when 'attachment'
736 when 'attachment'
737 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
737 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
738 if attachments && attachment = attachments.detect {|a| a.filename == name }
738 if attachments && attachment = attachments.detect {|a| a.filename == name }
739 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
739 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
740 :class => 'attachment'
740 :class => 'attachment'
741 end
741 end
742 when 'project'
742 when 'project'
743 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
743 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
744 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
744 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
745 end
745 end
746 end
746 end
747 end
747 end
748 end
748 end
749 (leading + (link || "#{project_prefix}#{prefix}#{sep}#{identifier}")).html_safe
749 (leading + (link || "#{project_prefix}#{prefix}#{sep}#{identifier}")).html_safe
750 end
750 end
751 end
751 end
752
752
753 HEADING_RE = /(<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>)/i unless const_defined?(:HEADING_RE)
753 HEADING_RE = /(<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>)/i unless const_defined?(:HEADING_RE)
754
754
755 def parse_sections(text, project, obj, attr, only_path, options)
755 def parse_sections(text, project, obj, attr, only_path, options)
756 return unless options[:edit_section_links]
756 return unless options[:edit_section_links]
757 text.gsub!(HEADING_RE) do
757 text.gsub!(HEADING_RE) do
758 @current_section += 1
758 @current_section += 1
759 if @current_section > 1
759 if @current_section > 1
760 content_tag('div',
760 content_tag('div',
761 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
761 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
762 :class => 'contextual',
762 :class => 'contextual',
763 :title => l(:button_edit_section)) + $1
763 :title => l(:button_edit_section)) + $1
764 else
764 else
765 $1
765 $1
766 end
766 end
767 end
767 end
768 end
768 end
769
769
770 # Headings and TOC
770 # Headings and TOC
771 # Adds ids and links to headings unless options[:headings] is set to false
771 # Adds ids and links to headings unless options[:headings] is set to false
772 def parse_headings(text, project, obj, attr, only_path, options)
772 def parse_headings(text, project, obj, attr, only_path, options)
773 return if options[:headings] == false
773 return if options[:headings] == false
774
774
775 text.gsub!(HEADING_RE) do
775 text.gsub!(HEADING_RE) do
776 level, attrs, content = $2.to_i, $3, $4
776 level, attrs, content = $2.to_i, $3, $4
777 item = strip_tags(content).strip
777 item = strip_tags(content).strip
778 anchor = sanitize_anchor_name(item)
778 anchor = sanitize_anchor_name(item)
779 # used for single-file wiki export
779 # used for single-file wiki export
780 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
780 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
781 @parsed_headings << [level, anchor, item]
781 @parsed_headings << [level, anchor, item]
782 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
782 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
783 end
783 end
784 end
784 end
785
785
786 MACROS_RE = /
786 MACROS_RE = /
787 (!)? # escaping
787 (!)? # escaping
788 (
788 (
789 \{\{ # opening tag
789 \{\{ # opening tag
790 ([\w]+) # macro name
790 ([\w]+) # macro name
791 (\(([^\}]*)\))? # optional arguments
791 (\(([^\}]*)\))? # optional arguments
792 \}\} # closing tag
792 \}\} # closing tag
793 )
793 )
794 /x unless const_defined?(:MACROS_RE)
794 /x unless const_defined?(:MACROS_RE)
795
795
796 # Macros substitution
796 # Macros substitution
797 def parse_macros(text, project, obj, attr, only_path, options)
797 def parse_macros(text, project, obj, attr, only_path, options)
798 text.gsub!(MACROS_RE) do
798 text.gsub!(MACROS_RE) do
799 esc, all, macro = $1, $2, $3.downcase
799 esc, all, macro = $1, $2, $3.downcase
800 args = ($5 || '').split(',').each(&:strip)
800 args = ($5 || '').split(',').each(&:strip)
801 if esc.nil?
801 if esc.nil?
802 begin
802 begin
803 exec_macro(macro, obj, args)
803 exec_macro(macro, obj, args)
804 rescue => e
804 rescue => e
805 "<div class=\"flash error\">Error executing the <strong>#{macro}</strong> macro (#{e})</div>"
805 "<div class=\"flash error\">Error executing the <strong>#{macro}</strong> macro (#{e})</div>"
806 end || all
806 end || all
807 else
807 else
808 all
808 all
809 end
809 end
810 end
810 end
811 end
811 end
812
812
813 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
813 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
814
814
815 # Renders the TOC with given headings
815 # Renders the TOC with given headings
816 def replace_toc(text, headings)
816 def replace_toc(text, headings)
817 text.gsub!(TOC_RE) do
817 text.gsub!(TOC_RE) do
818 if headings.empty?
818 if headings.empty?
819 ''
819 ''
820 else
820 else
821 div_class = 'toc'
821 div_class = 'toc'
822 div_class << ' right' if $1 == '>'
822 div_class << ' right' if $1 == '>'
823 div_class << ' left' if $1 == '<'
823 div_class << ' left' if $1 == '<'
824 out = "<ul class=\"#{div_class}\"><li>"
824 out = "<ul class=\"#{div_class}\"><li>"
825 root = headings.map(&:first).min
825 root = headings.map(&:first).min
826 current = root
826 current = root
827 started = false
827 started = false
828 headings.each do |level, anchor, item|
828 headings.each do |level, anchor, item|
829 if level > current
829 if level > current
830 out << '<ul><li>' * (level - current)
830 out << '<ul><li>' * (level - current)
831 elsif level < current
831 elsif level < current
832 out << "</li></ul>\n" * (current - level) + "</li><li>"
832 out << "</li></ul>\n" * (current - level) + "</li><li>"
833 elsif started
833 elsif started
834 out << '</li><li>'
834 out << '</li><li>'
835 end
835 end
836 out << "<a href=\"##{anchor}\">#{item}</a>"
836 out << "<a href=\"##{anchor}\">#{item}</a>"
837 current = level
837 current = level
838 started = true
838 started = true
839 end
839 end
840 out << '</li></ul>' * (current - root)
840 out << '</li></ul>' * (current - root)
841 out << '</li></ul>'
841 out << '</li></ul>'
842 end
842 end
843 end
843 end
844 end
844 end
845
845
846 # Same as Rails' simple_format helper without using paragraphs
846 # Same as Rails' simple_format helper without using paragraphs
847 def simple_format_without_paragraph(text)
847 def simple_format_without_paragraph(text)
848 text.to_s.
848 text.to_s.
849 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
849 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
850 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
850 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
851 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
851 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
852 html_safe
852 html_safe
853 end
853 end
854
854
855 def lang_options_for_select(blank=true)
855 def lang_options_for_select(blank=true)
856 (blank ? [["(auto)", ""]] : []) +
856 (blank ? [["(auto)", ""]] : []) +
857 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
857 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
858 end
858 end
859
859
860 def label_tag_for(name, option_tags = nil, options = {})
860 def label_tag_for(name, option_tags = nil, options = {})
861 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
861 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
862 content_tag("label", label_text)
862 content_tag("label", label_text)
863 end
863 end
864
864
865 def labelled_tabular_form_for(name, object, options, &proc)
865 def labelled_tabular_form_for(name, object, options, &proc)
866 options[:html] ||= {}
866 options[:html] ||= {}
867 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
867 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
868 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
868 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
869 end
869 end
870
870
871 def back_url_hidden_field_tag
871 def back_url_hidden_field_tag
872 back_url = params[:back_url] || request.env['HTTP_REFERER']
872 back_url = params[:back_url] || request.env['HTTP_REFERER']
873 back_url = CGI.unescape(back_url.to_s)
873 back_url = CGI.unescape(back_url.to_s)
874 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
874 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
875 end
875 end
876
876
877 def check_all_links(form_name)
877 def check_all_links(form_name)
878 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
878 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
879 " | ".html_safe +
879 " | ".html_safe +
880 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
880 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
881 end
881 end
882
882
883 def progress_bar(pcts, options={})
883 def progress_bar(pcts, options={})
884 pcts = [pcts, pcts] unless pcts.is_a?(Array)
884 pcts = [pcts, pcts] unless pcts.is_a?(Array)
885 pcts = pcts.collect(&:round)
885 pcts = pcts.collect(&:round)
886 pcts[1] = pcts[1] - pcts[0]
886 pcts[1] = pcts[1] - pcts[0]
887 pcts << (100 - pcts[1] - pcts[0])
887 pcts << (100 - pcts[1] - pcts[0])
888 width = options[:width] || '100px;'
888 width = options[:width] || '100px;'
889 legend = options[:legend] || ''
889 legend = options[:legend] || ''
890 content_tag('table',
890 content_tag('table',
891 content_tag('tr',
891 content_tag('tr',
892 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
892 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
893 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
893 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
894 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
894 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
895 ), :class => 'progress', :style => "width: #{width};").html_safe +
895 ), :class => 'progress', :style => "width: #{width};").html_safe +
896 content_tag('p', legend, :class => 'pourcent').html_safe
896 content_tag('p', legend, :class => 'pourcent').html_safe
897 end
897 end
898
898
899 def checked_image(checked=true)
899 def checked_image(checked=true)
900 if checked
900 if checked
901 image_tag 'toggle_check.png'
901 image_tag 'toggle_check.png'
902 end
902 end
903 end
903 end
904
904
905 def context_menu(url)
905 def context_menu(url)
906 unless @context_menu_included
906 unless @context_menu_included
907 content_for :header_tags do
907 content_for :header_tags do
908 javascript_include_tag('context_menu') +
908 javascript_include_tag('context_menu') +
909 stylesheet_link_tag('context_menu')
909 stylesheet_link_tag('context_menu')
910 end
910 end
911 if l(:direction) == 'rtl'
911 if l(:direction) == 'rtl'
912 content_for :header_tags do
912 content_for :header_tags do
913 stylesheet_link_tag('context_menu_rtl')
913 stylesheet_link_tag('context_menu_rtl')
914 end
914 end
915 end
915 end
916 @context_menu_included = true
916 @context_menu_included = true
917 end
917 end
918 javascript_tag "new ContextMenu('#{ url_for(url) }')"
918 javascript_tag "new ContextMenu('#{ url_for(url) }')"
919 end
919 end
920
920
921 def context_menu_link(name, url, options={})
921 def context_menu_link(name, url, options={})
922 options[:class] ||= ''
922 options[:class] ||= ''
923 if options.delete(:selected)
923 if options.delete(:selected)
924 options[:class] << ' icon-checked disabled'
924 options[:class] << ' icon-checked disabled'
925 options[:disabled] = true
925 options[:disabled] = true
926 end
926 end
927 if options.delete(:disabled)
927 if options.delete(:disabled)
928 options.delete(:method)
928 options.delete(:method)
929 options.delete(:confirm)
929 options.delete(:confirm)
930 options.delete(:onclick)
930 options.delete(:onclick)
931 options[:class] << ' disabled'
931 options[:class] << ' disabled'
932 url = '#'
932 url = '#'
933 end
933 end
934 link_to h(name), url, options
934 link_to h(name), url, options
935 end
935 end
936
936
937 def calendar_for(field_id)
937 def calendar_for(field_id)
938 include_calendar_headers_tags
938 include_calendar_headers_tags
939 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
939 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
940 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
940 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
941 end
941 end
942
942
943 def include_calendar_headers_tags
943 def include_calendar_headers_tags
944 unless @calendar_headers_tags_included
944 unless @calendar_headers_tags_included
945 @calendar_headers_tags_included = true
945 @calendar_headers_tags_included = true
946 content_for :header_tags do
946 content_for :header_tags do
947 start_of_week = case Setting.start_of_week.to_i
947 start_of_week = case Setting.start_of_week.to_i
948 when 1
948 when 1
949 'Calendar._FD = 1;' # Monday
949 'Calendar._FD = 1;' # Monday
950 when 7
950 when 7
951 'Calendar._FD = 0;' # Sunday
951 'Calendar._FD = 0;' # Sunday
952 when 6
952 when 6
953 'Calendar._FD = 6;' # Saturday
953 'Calendar._FD = 6;' # Saturday
954 else
954 else
955 '' # use language
955 '' # use language
956 end
956 end
957
957
958 javascript_include_tag('calendar/calendar') +
958 javascript_include_tag('calendar/calendar') +
959 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
959 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
960 javascript_tag(start_of_week) +
960 javascript_tag(start_of_week) +
961 javascript_include_tag('calendar/calendar-setup') +
961 javascript_include_tag('calendar/calendar-setup') +
962 stylesheet_link_tag('calendar')
962 stylesheet_link_tag('calendar')
963 end
963 end
964 end
964 end
965 end
965 end
966
966
967 def content_for(name, content = nil, &block)
967 def content_for(name, content = nil, &block)
968 @has_content ||= {}
968 @has_content ||= {}
969 @has_content[name] = true
969 @has_content[name] = true
970 super(name, content, &block)
970 super(name, content, &block)
971 end
971 end
972
972
973 def has_content?(name)
973 def has_content?(name)
974 (@has_content && @has_content[name]) || false
974 (@has_content && @has_content[name]) || false
975 end
975 end
976
976
977 def email_delivery_enabled?
977 def email_delivery_enabled?
978 !!ActionMailer::Base.perform_deliveries
978 !!ActionMailer::Base.perform_deliveries
979 end
979 end
980
980
981 # Returns the avatar image tag for the given +user+ if avatars are enabled
981 # Returns the avatar image tag for the given +user+ if avatars are enabled
982 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
982 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
983 def avatar(user, options = { })
983 def avatar(user, options = { })
984 if Setting.gravatar_enabled?
984 if Setting.gravatar_enabled?
985 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
985 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
986 email = nil
986 email = nil
987 if user.respond_to?(:mail)
987 if user.respond_to?(:mail)
988 email = user.mail
988 email = user.mail
989 elsif user.to_s =~ %r{<(.+?)>}
989 elsif user.to_s =~ %r{<(.+?)>}
990 email = $1
990 email = $1
991 end
991 end
992 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
992 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
993 else
993 else
994 ''
994 ''
995 end
995 end
996 end
996 end
997
997
998 def sanitize_anchor_name(anchor)
998 def sanitize_anchor_name(anchor)
999 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
999 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1000 end
1000 end
1001
1001
1002 # Returns the javascript tags that are included in the html layout head
1002 # Returns the javascript tags that are included in the html layout head
1003 def javascript_heads
1003 def javascript_heads
1004 tags = javascript_include_tag(:defaults)
1004 tags = javascript_include_tag(:defaults)
1005 unless User.current.pref.warn_on_leaving_unsaved == '0'
1005 unless User.current.pref.warn_on_leaving_unsaved == '0'
1006 tags << "\n".html_safe + javascript_tag("Event.observe(window, 'load', function(){ new WarnLeavingUnsaved('#{escape_javascript( l(:text_warn_on_leaving_unsaved) )}'); });")
1006 tags << "\n".html_safe + javascript_tag("Event.observe(window, 'load', function(){ new WarnLeavingUnsaved('#{escape_javascript( l(:text_warn_on_leaving_unsaved) )}'); });")
1007 end
1007 end
1008 tags
1008 tags
1009 end
1009 end
1010
1010
1011 def favicon
1011 def favicon
1012 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1012 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1013 end
1013 end
1014
1014
1015 def robot_exclusion_tag
1015 def robot_exclusion_tag
1016 '<meta name="robots" content="noindex,follow,noarchive" />'
1016 '<meta name="robots" content="noindex,follow,noarchive" />'
1017 end
1017 end
1018
1018
1019 # Returns true if arg is expected in the API response
1019 # Returns true if arg is expected in the API response
1020 def include_in_api_response?(arg)
1020 def include_in_api_response?(arg)
1021 unless @included_in_api_response
1021 unless @included_in_api_response
1022 param = params[:include]
1022 param = params[:include]
1023 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1023 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1024 @included_in_api_response.collect!(&:strip)
1024 @included_in_api_response.collect!(&:strip)
1025 end
1025 end
1026 @included_in_api_response.include?(arg.to_s)
1026 @included_in_api_response.include?(arg.to_s)
1027 end
1027 end
1028
1028
1029 # Returns options or nil if nometa param or X-Redmine-Nometa header
1029 # Returns options or nil if nometa param or X-Redmine-Nometa header
1030 # was set in the request
1030 # was set in the request
1031 def api_meta(options)
1031 def api_meta(options)
1032 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1032 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1033 # compatibility mode for activeresource clients that raise
1033 # compatibility mode for activeresource clients that raise
1034 # an error when unserializing an array with attributes
1034 # an error when unserializing an array with attributes
1035 nil
1035 nil
1036 else
1036 else
1037 options
1037 options
1038 end
1038 end
1039 end
1039 end
1040
1040
1041 private
1041 private
1042
1042
1043 def wiki_helper
1043 def wiki_helper
1044 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1044 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1045 extend helper
1045 extend helper
1046 return self
1046 return self
1047 end
1047 end
1048
1048
1049 def link_to_content_update(text, url_params = {}, html_options = {})
1049 def link_to_content_update(text, url_params = {}, html_options = {})
1050 link_to(text, url_params, html_options)
1050 link_to(text, url_params, html_options)
1051 end
1051 end
1052 end
1052 end
@@ -1,197 +1,197
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 "digest/md5"
18 require "digest/md5"
19
19
20 class Attachment < ActiveRecord::Base
20 class Attachment < ActiveRecord::Base
21 belongs_to :container, :polymorphic => true
21 belongs_to :container, :polymorphic => true
22 belongs_to :author, :class_name => "User", :foreign_key => "author_id"
22 belongs_to :author, :class_name => "User", :foreign_key => "author_id"
23
23
24 validates_presence_of :container, :filename, :author
24 validates_presence_of :container, :filename, :author
25 validates_length_of :filename, :maximum => 255
25 validates_length_of :filename, :maximum => 255
26 validates_length_of :disk_filename, :maximum => 255
26 validates_length_of :disk_filename, :maximum => 255
27 validate :validate_max_file_size
27 validate :validate_max_file_size
28
28
29 acts_as_event :title => :filename,
29 acts_as_event :title => :filename,
30 :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}}
30 :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}}
31
31
32 acts_as_activity_provider :type => 'files',
32 acts_as_activity_provider :type => 'files',
33 :permission => :view_files,
33 :permission => :view_files,
34 :author_key => :author_id,
34 :author_key => :author_id,
35 :find_options => {:select => "#{Attachment.table_name}.*",
35 :find_options => {:select => "#{Attachment.table_name}.*",
36 :joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
36 :joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
37 "LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id OR ( #{Attachment.table_name}.container_type='Project' AND #{Attachment.table_name}.container_id = #{Project.table_name}.id )"}
37 "LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id OR ( #{Attachment.table_name}.container_type='Project' AND #{Attachment.table_name}.container_id = #{Project.table_name}.id )"}
38
38
39 acts_as_activity_provider :type => 'documents',
39 acts_as_activity_provider :type => 'documents',
40 :permission => :view_documents,
40 :permission => :view_documents,
41 :author_key => :author_id,
41 :author_key => :author_id,
42 :find_options => {:select => "#{Attachment.table_name}.*",
42 :find_options => {:select => "#{Attachment.table_name}.*",
43 :joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
43 :joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
44 "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"}
44 "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"}
45
45
46 cattr_accessor :storage_path
46 cattr_accessor :storage_path
47 @@storage_path = Redmine::Configuration['attachments_storage_path'] || "#{Rails.root}/files"
47 @@storage_path = Redmine::Configuration['attachments_storage_path'] || "#{Rails.root}/files"
48
48
49 before_save :files_to_final_location
49 before_save :files_to_final_location
50 after_destroy :delete_from_disk
50 after_destroy :delete_from_disk
51
51
52 def validate_max_file_size
52 def validate_max_file_size
53 if self.filesize > Setting.attachment_max_size.to_i.kilobytes
53 if self.filesize > Setting.attachment_max_size.to_i.kilobytes
54 errors.add(:base, :too_long, :count => Setting.attachment_max_size.to_i.kilobytes)
54 errors.add(:base, :too_long, :count => Setting.attachment_max_size.to_i.kilobytes)
55 end
55 end
56 end
56 end
57
57
58 def file=(incoming_file)
58 def file=(incoming_file)
59 unless incoming_file.nil?
59 unless incoming_file.nil?
60 @temp_file = incoming_file
60 @temp_file = incoming_file
61 if @temp_file.size > 0
61 if @temp_file.size > 0
62 self.filename = sanitize_filename(@temp_file.original_filename)
62 self.filename = sanitize_filename(@temp_file.original_filename)
63 self.disk_filename = Attachment.disk_filename(filename)
63 self.disk_filename = Attachment.disk_filename(filename)
64 self.content_type = @temp_file.content_type.to_s.chomp
64 self.content_type = @temp_file.content_type.to_s.chomp
65 if content_type.blank?
65 if content_type.blank?
66 self.content_type = Redmine::MimeType.of(filename)
66 self.content_type = Redmine::MimeType.of(filename)
67 end
67 end
68 self.filesize = @temp_file.size
68 self.filesize = @temp_file.size
69 end
69 end
70 end
70 end
71 end
71 end
72
72
73 def file
73 def file
74 nil
74 nil
75 end
75 end
76
76
77 # Copies the temporary file to its final location
77 # Copies the temporary file to its final location
78 # and computes its MD5 hash
78 # and computes its MD5 hash
79 def files_to_final_location
79 def files_to_final_location
80 if @temp_file && (@temp_file.size > 0)
80 if @temp_file && (@temp_file.size > 0)
81 logger.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)")
81 logger.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)")
82 md5 = Digest::MD5.new
82 md5 = Digest::MD5.new
83 File.open(diskfile, "wb") do |f|
83 File.open(diskfile, "wb") do |f|
84 buffer = ""
84 buffer = ""
85 while (buffer = @temp_file.read(8192))
85 while (buffer = @temp_file.read(8192))
86 f.write(buffer)
86 f.write(buffer)
87 md5.update(buffer)
87 md5.update(buffer)
88 end
88 end
89 end
89 end
90 self.digest = md5.hexdigest
90 self.digest = md5.hexdigest
91 end
91 end
92 @temp_file = nil
92 @temp_file = nil
93 # Don't save the content type if it's longer than the authorized length
93 # Don't save the content type if it's longer than the authorized length
94 if self.content_type && self.content_type.length > 255
94 if self.content_type && self.content_type.length > 255
95 self.content_type = nil
95 self.content_type = nil
96 end
96 end
97 end
97 end
98
98
99 # Deletes file on the disk
99 # Deletes file on the disk
100 def delete_from_disk
100 def delete_from_disk
101 File.delete(diskfile) if !filename.blank? && File.exist?(diskfile)
101 File.delete(diskfile) if !filename.blank? && File.exist?(diskfile)
102 end
102 end
103
103
104 # Returns file's location on disk
104 # Returns file's location on disk
105 def diskfile
105 def diskfile
106 "#{@@storage_path}/#{self.disk_filename}"
106 "#{@@storage_path}/#{self.disk_filename}"
107 end
107 end
108
108
109 def increment_download
109 def increment_download
110 increment!(:downloads)
110 increment!(:downloads)
111 end
111 end
112
112
113 def project
113 def project
114 container.project
114 container.project
115 end
115 end
116
116
117 def visible?(user=User.current)
117 def visible?(user=User.current)
118 container.attachments_visible?(user)
118 container.attachments_visible?(user)
119 end
119 end
120
120
121 def deletable?(user=User.current)
121 def deletable?(user=User.current)
122 container.attachments_deletable?(user)
122 container.attachments_deletable?(user)
123 end
123 end
124
124
125 def image?
125 def image?
126 self.filename =~ /\.(jpe?g|gif|png)$/i
126 self.filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png)$/i
127 end
127 end
128
128
129 def is_text?
129 def is_text?
130 Redmine::MimeType.is_type?('text', filename)
130 Redmine::MimeType.is_type?('text', filename)
131 end
131 end
132
132
133 def is_diff?
133 def is_diff?
134 self.filename =~ /\.(patch|diff)$/i
134 self.filename =~ /\.(patch|diff)$/i
135 end
135 end
136
136
137 # Returns true if the file is readable
137 # Returns true if the file is readable
138 def readable?
138 def readable?
139 File.readable?(diskfile)
139 File.readable?(diskfile)
140 end
140 end
141
141
142 # Bulk attaches a set of files to an object
142 # Bulk attaches a set of files to an object
143 #
143 #
144 # Returns a Hash of the results:
144 # Returns a Hash of the results:
145 # :files => array of the attached files
145 # :files => array of the attached files
146 # :unsaved => array of the files that could not be attached
146 # :unsaved => array of the files that could not be attached
147 def self.attach_files(obj, attachments)
147 def self.attach_files(obj, attachments)
148 attached = []
148 attached = []
149 if attachments && attachments.is_a?(Hash)
149 if attachments && attachments.is_a?(Hash)
150 attachments.each_value do |attachment|
150 attachments.each_value do |attachment|
151 file = attachment['file']
151 file = attachment['file']
152 next unless file && file.size > 0
152 next unless file && file.size > 0
153 a = Attachment.create(:container => obj,
153 a = Attachment.create(:container => obj,
154 :file => file,
154 :file => file,
155 :description => attachment['description'].to_s.strip,
155 :description => attachment['description'].to_s.strip,
156 :author => User.current)
156 :author => User.current)
157 obj.attachments << a
157 obj.attachments << a
158
158
159 if a.new_record?
159 if a.new_record?
160 obj.unsaved_attachments ||= []
160 obj.unsaved_attachments ||= []
161 obj.unsaved_attachments << a
161 obj.unsaved_attachments << a
162 else
162 else
163 attached << a
163 attached << a
164 end
164 end
165 end
165 end
166 end
166 end
167 {:files => attached, :unsaved => obj.unsaved_attachments}
167 {:files => attached, :unsaved => obj.unsaved_attachments}
168 end
168 end
169
169
170 private
170 private
171 def sanitize_filename(value)
171 def sanitize_filename(value)
172 # get only the filename, not the whole path
172 # get only the filename, not the whole path
173 just_filename = value.gsub(/^.*(\\|\/)/, '')
173 just_filename = value.gsub(/^.*(\\|\/)/, '')
174 # NOTE: File.basename doesn't work right with Windows paths on Unix
174 # NOTE: File.basename doesn't work right with Windows paths on Unix
175 # INCORRECT: just_filename = File.basename(value.gsub('\\\\', '/'))
175 # INCORRECT: just_filename = File.basename(value.gsub('\\\\', '/'))
176
176
177 # Finally, replace all non alphanumeric, hyphens or periods with underscore
177 # Finally, replace all non alphanumeric, hyphens or periods with underscore
178 @filename = just_filename.gsub(/[^\w\.\-]/,'_')
178 @filename = just_filename.gsub(/[^\w\.\-]/,'_')
179 end
179 end
180
180
181 # Returns an ASCII or hashed filename
181 # Returns an ASCII or hashed filename
182 def self.disk_filename(filename)
182 def self.disk_filename(filename)
183 timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
183 timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
184 ascii = ''
184 ascii = ''
185 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
185 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
186 ascii = filename
186 ascii = filename
187 else
187 else
188 ascii = Digest::MD5.hexdigest(filename)
188 ascii = Digest::MD5.hexdigest(filename)
189 # keep the extension if any
189 # keep the extension if any
190 ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
190 ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
191 end
191 end
192 while File.exist?(File.join(@@storage_path, "#{timestamp}_#{ascii}"))
192 while File.exist?(File.join(@@storage_path, "#{timestamp}_#{ascii}"))
193 timestamp.succ!
193 timestamp.succ!
194 end
194 end
195 "#{timestamp}_#{ascii}"
195 "#{timestamp}_#{ascii}"
196 end
196 end
197 end
197 end
@@ -1,449 +1,460
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 ENV["RAILS_ENV"] = "test"
18 ENV["RAILS_ENV"] = "test"
19 require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
19 require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
20 require 'test_help'
20 require 'test_help'
21 require File.expand_path(File.dirname(__FILE__) + '/helper_testcase')
21 require File.expand_path(File.dirname(__FILE__) + '/helper_testcase')
22 require Rails.root.join('test', 'mocks', 'open_id_authentication_mock.rb').to_s
22 require Rails.root.join('test', 'mocks', 'open_id_authentication_mock.rb').to_s
23
23
24 require File.expand_path(File.dirname(__FILE__) + '/object_daddy_helpers')
24 require File.expand_path(File.dirname(__FILE__) + '/object_daddy_helpers')
25 include ObjectDaddyHelpers
25 include ObjectDaddyHelpers
26
26
27 class ActiveSupport::TestCase
27 class ActiveSupport::TestCase
28 # Transactional fixtures accelerate your tests by wrapping each test method
28 # Transactional fixtures accelerate your tests by wrapping each test method
29 # in a transaction that's rolled back on completion. This ensures that the
29 # in a transaction that's rolled back on completion. This ensures that the
30 # test database remains unchanged so your fixtures don't have to be reloaded
30 # test database remains unchanged so your fixtures don't have to be reloaded
31 # between every test method. Fewer database queries means faster tests.
31 # between every test method. Fewer database queries means faster tests.
32 #
32 #
33 # Read Mike Clark's excellent walkthrough at
33 # Read Mike Clark's excellent walkthrough at
34 # http://clarkware.com/cgi/blosxom/2005/10/24#Rails10FastTesting
34 # http://clarkware.com/cgi/blosxom/2005/10/24#Rails10FastTesting
35 #
35 #
36 # Every Active Record database supports transactions except MyISAM tables
36 # Every Active Record database supports transactions except MyISAM tables
37 # in MySQL. Turn off transactional fixtures in this case; however, if you
37 # in MySQL. Turn off transactional fixtures in this case; however, if you
38 # don't care one way or the other, switching from MyISAM to InnoDB tables
38 # don't care one way or the other, switching from MyISAM to InnoDB tables
39 # is recommended.
39 # is recommended.
40 self.use_transactional_fixtures = true
40 self.use_transactional_fixtures = true
41
41
42 # Instantiated fixtures are slow, but give you @david where otherwise you
42 # Instantiated fixtures are slow, but give you @david where otherwise you
43 # would need people(:david). If you don't want to migrate your existing
43 # would need people(:david). If you don't want to migrate your existing
44 # test cases which use the @david style and don't mind the speed hit (each
44 # test cases which use the @david style and don't mind the speed hit (each
45 # instantiated fixtures translates to a database query per test method),
45 # instantiated fixtures translates to a database query per test method),
46 # then set this back to true.
46 # then set this back to true.
47 self.use_instantiated_fixtures = false
47 self.use_instantiated_fixtures = false
48
48
49 # Add more helper methods to be used by all tests here...
49 # Add more helper methods to be used by all tests here...
50
50
51 def log_user(login, password)
51 def log_user(login, password)
52 User.anonymous
52 User.anonymous
53 get "/login"
53 get "/login"
54 assert_equal nil, session[:user_id]
54 assert_equal nil, session[:user_id]
55 assert_response :success
55 assert_response :success
56 assert_template "account/login"
56 assert_template "account/login"
57 post "/login", :username => login, :password => password
57 post "/login", :username => login, :password => password
58 assert_equal login, User.find(session[:user_id]).login
58 assert_equal login, User.find(session[:user_id]).login
59 end
59 end
60
60
61 def uploaded_test_file(name, mime)
61 def uploaded_test_file(name, mime)
62 ActionController::TestUploadedFile.new(
62 ActionController::TestUploadedFile.new(
63 ActiveSupport::TestCase.fixture_path + "/files/#{name}", mime, true)
63 ActiveSupport::TestCase.fixture_path + "/files/#{name}", mime, true)
64 end
64 end
65
65
66 # Mock out a file
66 # Mock out a file
67 def self.mock_file
67 def self.mock_file
68 file = 'a_file.png'
68 file = 'a_file.png'
69 file.stubs(:size).returns(32)
69 file.stubs(:size).returns(32)
70 file.stubs(:original_filename).returns('a_file.png')
70 file.stubs(:original_filename).returns('a_file.png')
71 file.stubs(:content_type).returns('image/png')
71 file.stubs(:content_type).returns('image/png')
72 file.stubs(:read).returns(false)
72 file.stubs(:read).returns(false)
73 file
73 file
74 end
74 end
75
75
76 def mock_file
76 def mock_file
77 self.class.mock_file
77 self.class.mock_file
78 end
78 end
79
79
80 def mock_file_with_options(options={})
81 file = ''
82 file.stubs(:size).returns(32)
83 original_filename = options[:original_filename] || nil
84 file.stubs(:original_filename).returns(original_filename)
85 content_type = options[:content_type] || nil
86 file.stubs(:content_type).returns(content_type)
87 file.stubs(:read).returns(false)
88 file
89 end
90
80 # Use a temporary directory for attachment related tests
91 # Use a temporary directory for attachment related tests
81 def set_tmp_attachments_directory
92 def set_tmp_attachments_directory
82 Dir.mkdir "#{Rails.root}/tmp/test" unless File.directory?("#{Rails.root}/tmp/test")
93 Dir.mkdir "#{Rails.root}/tmp/test" unless File.directory?("#{Rails.root}/tmp/test")
83 unless File.directory?("#{Rails.root}/tmp/test/attachments")
94 unless File.directory?("#{Rails.root}/tmp/test/attachments")
84 Dir.mkdir "#{Rails.root}/tmp/test/attachments"
95 Dir.mkdir "#{Rails.root}/tmp/test/attachments"
85 end
96 end
86 Attachment.storage_path = "#{Rails.root}/tmp/test/attachments"
97 Attachment.storage_path = "#{Rails.root}/tmp/test/attachments"
87 end
98 end
88
99
89 def with_settings(options, &block)
100 def with_settings(options, &block)
90 saved_settings = options.keys.inject({}) {|h, k| h[k] = Setting[k].dup; h}
101 saved_settings = options.keys.inject({}) {|h, k| h[k] = Setting[k].dup; h}
91 options.each {|k, v| Setting[k] = v}
102 options.each {|k, v| Setting[k] = v}
92 yield
103 yield
93 ensure
104 ensure
94 saved_settings.each {|k, v| Setting[k] = v}
105 saved_settings.each {|k, v| Setting[k] = v}
95 end
106 end
96
107
97 def change_user_password(login, new_password)
108 def change_user_password(login, new_password)
98 user = User.first(:conditions => {:login => login})
109 user = User.first(:conditions => {:login => login})
99 user.password, user.password_confirmation = new_password, new_password
110 user.password, user.password_confirmation = new_password, new_password
100 user.save!
111 user.save!
101 end
112 end
102
113
103 def self.ldap_configured?
114 def self.ldap_configured?
104 @test_ldap = Net::LDAP.new(:host => '127.0.0.1', :port => 389)
115 @test_ldap = Net::LDAP.new(:host => '127.0.0.1', :port => 389)
105 return @test_ldap.bind
116 return @test_ldap.bind
106 rescue Exception => e
117 rescue Exception => e
107 # LDAP is not listening
118 # LDAP is not listening
108 return nil
119 return nil
109 end
120 end
110
121
111 # Returns the path to the test +vendor+ repository
122 # Returns the path to the test +vendor+ repository
112 def self.repository_path(vendor)
123 def self.repository_path(vendor)
113 Rails.root.join("tmp/test/#{vendor.downcase}_repository").to_s
124 Rails.root.join("tmp/test/#{vendor.downcase}_repository").to_s
114 end
125 end
115
126
116 # Returns the url of the subversion test repository
127 # Returns the url of the subversion test repository
117 def self.subversion_repository_url
128 def self.subversion_repository_url
118 path = repository_path('subversion')
129 path = repository_path('subversion')
119 path = '/' + path unless path.starts_with?('/')
130 path = '/' + path unless path.starts_with?('/')
120 "file://#{path}"
131 "file://#{path}"
121 end
132 end
122
133
123 # Returns true if the +vendor+ test repository is configured
134 # Returns true if the +vendor+ test repository is configured
124 def self.repository_configured?(vendor)
135 def self.repository_configured?(vendor)
125 File.directory?(repository_path(vendor))
136 File.directory?(repository_path(vendor))
126 end
137 end
127
138
128 def assert_error_tag(options={})
139 def assert_error_tag(options={})
129 assert_tag({:attributes => { :id => 'errorExplanation' }}.merge(options))
140 assert_tag({:attributes => { :id => 'errorExplanation' }}.merge(options))
130 end
141 end
131
142
132 def assert_include(expected, s)
143 def assert_include(expected, s)
133 assert s.include?(expected), "\"#{expected}\" not found in \"#{s}\""
144 assert s.include?(expected), "\"#{expected}\" not found in \"#{s}\""
134 end
145 end
135
146
136 # Shoulda macros
147 # Shoulda macros
137 def self.should_render_404
148 def self.should_render_404
138 should_respond_with :not_found
149 should_respond_with :not_found
139 should_render_template 'common/error'
150 should_render_template 'common/error'
140 end
151 end
141
152
142 def self.should_have_before_filter(expected_method, options = {})
153 def self.should_have_before_filter(expected_method, options = {})
143 should_have_filter('before', expected_method, options)
154 should_have_filter('before', expected_method, options)
144 end
155 end
145
156
146 def self.should_have_after_filter(expected_method, options = {})
157 def self.should_have_after_filter(expected_method, options = {})
147 should_have_filter('after', expected_method, options)
158 should_have_filter('after', expected_method, options)
148 end
159 end
149
160
150 def self.should_have_filter(filter_type, expected_method, options)
161 def self.should_have_filter(filter_type, expected_method, options)
151 description = "have #{filter_type}_filter :#{expected_method}"
162 description = "have #{filter_type}_filter :#{expected_method}"
152 description << " with #{options.inspect}" unless options.empty?
163 description << " with #{options.inspect}" unless options.empty?
153
164
154 should description do
165 should description do
155 klass = "action_controller/filters/#{filter_type}_filter".classify.constantize
166 klass = "action_controller/filters/#{filter_type}_filter".classify.constantize
156 expected = klass.new(:filter, expected_method.to_sym, options)
167 expected = klass.new(:filter, expected_method.to_sym, options)
157 assert_equal 1, @controller.class.filter_chain.select { |filter|
168 assert_equal 1, @controller.class.filter_chain.select { |filter|
158 filter.method == expected.method && filter.kind == expected.kind &&
169 filter.method == expected.method && filter.kind == expected.kind &&
159 filter.options == expected.options && filter.class == expected.class
170 filter.options == expected.options && filter.class == expected.class
160 }.size
171 }.size
161 end
172 end
162 end
173 end
163
174
164 def self.should_show_the_old_and_new_values_for(prop_key, model, &block)
175 def self.should_show_the_old_and_new_values_for(prop_key, model, &block)
165 context "" do
176 context "" do
166 setup do
177 setup do
167 if block_given?
178 if block_given?
168 instance_eval &block
179 instance_eval &block
169 else
180 else
170 @old_value = model.generate!
181 @old_value = model.generate!
171 @new_value = model.generate!
182 @new_value = model.generate!
172 end
183 end
173 end
184 end
174
185
175 should "use the new value's name" do
186 should "use the new value's name" do
176 @detail = JournalDetail.generate!(:property => 'attr',
187 @detail = JournalDetail.generate!(:property => 'attr',
177 :old_value => @old_value.id,
188 :old_value => @old_value.id,
178 :value => @new_value.id,
189 :value => @new_value.id,
179 :prop_key => prop_key)
190 :prop_key => prop_key)
180
191
181 assert_match @new_value.name, show_detail(@detail, true)
192 assert_match @new_value.name, show_detail(@detail, true)
182 end
193 end
183
194
184 should "use the old value's name" do
195 should "use the old value's name" do
185 @detail = JournalDetail.generate!(:property => 'attr',
196 @detail = JournalDetail.generate!(:property => 'attr',
186 :old_value => @old_value.id,
197 :old_value => @old_value.id,
187 :value => @new_value.id,
198 :value => @new_value.id,
188 :prop_key => prop_key)
199 :prop_key => prop_key)
189
200
190 assert_match @old_value.name, show_detail(@detail, true)
201 assert_match @old_value.name, show_detail(@detail, true)
191 end
202 end
192 end
203 end
193 end
204 end
194
205
195 def self.should_create_a_new_user(&block)
206 def self.should_create_a_new_user(&block)
196 should "create a new user" do
207 should "create a new user" do
197 user = instance_eval &block
208 user = instance_eval &block
198 assert user
209 assert user
199 assert_kind_of User, user
210 assert_kind_of User, user
200 assert !user.new_record?
211 assert !user.new_record?
201 end
212 end
202 end
213 end
203
214
204 # Test that a request allows the three types of API authentication
215 # Test that a request allows the three types of API authentication
205 #
216 #
206 # * HTTP Basic with username and password
217 # * HTTP Basic with username and password
207 # * HTTP Basic with an api key for the username
218 # * HTTP Basic with an api key for the username
208 # * Key based with the key=X parameter
219 # * Key based with the key=X parameter
209 #
220 #
210 # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete)
221 # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete)
211 # @param [String] url the request url
222 # @param [String] url the request url
212 # @param [optional, Hash] parameters additional request parameters
223 # @param [optional, Hash] parameters additional request parameters
213 # @param [optional, Hash] options additional options
224 # @param [optional, Hash] options additional options
214 # @option options [Symbol] :success_code Successful response code (:success)
225 # @option options [Symbol] :success_code Successful response code (:success)
215 # @option options [Symbol] :failure_code Failure response code (:unauthorized)
226 # @option options [Symbol] :failure_code Failure response code (:unauthorized)
216 def self.should_allow_api_authentication(http_method, url, parameters={}, options={})
227 def self.should_allow_api_authentication(http_method, url, parameters={}, options={})
217 should_allow_http_basic_auth_with_username_and_password(http_method, url, parameters, options)
228 should_allow_http_basic_auth_with_username_and_password(http_method, url, parameters, options)
218 should_allow_http_basic_auth_with_key(http_method, url, parameters, options)
229 should_allow_http_basic_auth_with_key(http_method, url, parameters, options)
219 should_allow_key_based_auth(http_method, url, parameters, options)
230 should_allow_key_based_auth(http_method, url, parameters, options)
220 end
231 end
221
232
222 # Test that a request allows the username and password for HTTP BASIC
233 # Test that a request allows the username and password for HTTP BASIC
223 #
234 #
224 # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete)
235 # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete)
225 # @param [String] url the request url
236 # @param [String] url the request url
226 # @param [optional, Hash] parameters additional request parameters
237 # @param [optional, Hash] parameters additional request parameters
227 # @param [optional, Hash] options additional options
238 # @param [optional, Hash] options additional options
228 # @option options [Symbol] :success_code Successful response code (:success)
239 # @option options [Symbol] :success_code Successful response code (:success)
229 # @option options [Symbol] :failure_code Failure response code (:unauthorized)
240 # @option options [Symbol] :failure_code Failure response code (:unauthorized)
230 def self.should_allow_http_basic_auth_with_username_and_password(http_method, url, parameters={}, options={})
241 def self.should_allow_http_basic_auth_with_username_and_password(http_method, url, parameters={}, options={})
231 success_code = options[:success_code] || :success
242 success_code = options[:success_code] || :success
232 failure_code = options[:failure_code] || :unauthorized
243 failure_code = options[:failure_code] || :unauthorized
233
244
234 context "should allow http basic auth using a username and password for #{http_method} #{url}" do
245 context "should allow http basic auth using a username and password for #{http_method} #{url}" do
235 context "with a valid HTTP authentication" do
246 context "with a valid HTTP authentication" do
236 setup do
247 setup do
237 @user = User.generate_with_protected!(:password => 'my_password', :password_confirmation => 'my_password', :admin => true) # Admin so they can access the project
248 @user = User.generate_with_protected!(:password => 'my_password', :password_confirmation => 'my_password', :admin => true) # Admin so they can access the project
238 @authorization = ActionController::HttpAuthentication::Basic.encode_credentials(@user.login, 'my_password')
249 @authorization = ActionController::HttpAuthentication::Basic.encode_credentials(@user.login, 'my_password')
239 send(http_method, url, parameters, {:authorization => @authorization})
250 send(http_method, url, parameters, {:authorization => @authorization})
240 end
251 end
241
252
242 should_respond_with success_code
253 should_respond_with success_code
243 should_respond_with_content_type_based_on_url(url)
254 should_respond_with_content_type_based_on_url(url)
244 should "login as the user" do
255 should "login as the user" do
245 assert_equal @user, User.current
256 assert_equal @user, User.current
246 end
257 end
247 end
258 end
248
259
249 context "with an invalid HTTP authentication" do
260 context "with an invalid HTTP authentication" do
250 setup do
261 setup do
251 @user = User.generate_with_protected!
262 @user = User.generate_with_protected!
252 @authorization = ActionController::HttpAuthentication::Basic.encode_credentials(@user.login, 'wrong_password')
263 @authorization = ActionController::HttpAuthentication::Basic.encode_credentials(@user.login, 'wrong_password')
253 send(http_method, url, parameters, {:authorization => @authorization})
264 send(http_method, url, parameters, {:authorization => @authorization})
254 end
265 end
255
266
256 should_respond_with failure_code
267 should_respond_with failure_code
257 should_respond_with_content_type_based_on_url(url)
268 should_respond_with_content_type_based_on_url(url)
258 should "not login as the user" do
269 should "not login as the user" do
259 assert_equal User.anonymous, User.current
270 assert_equal User.anonymous, User.current
260 end
271 end
261 end
272 end
262
273
263 context "without credentials" do
274 context "without credentials" do
264 setup do
275 setup do
265 send(http_method, url, parameters, {:authorization => ''})
276 send(http_method, url, parameters, {:authorization => ''})
266 end
277 end
267
278
268 should_respond_with failure_code
279 should_respond_with failure_code
269 should_respond_with_content_type_based_on_url(url)
280 should_respond_with_content_type_based_on_url(url)
270 should "include_www_authenticate_header" do
281 should "include_www_authenticate_header" do
271 assert @controller.response.headers.has_key?('WWW-Authenticate')
282 assert @controller.response.headers.has_key?('WWW-Authenticate')
272 end
283 end
273 end
284 end
274 end
285 end
275
286
276 end
287 end
277
288
278 # Test that a request allows the API key with HTTP BASIC
289 # Test that a request allows the API key with HTTP BASIC
279 #
290 #
280 # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete)
291 # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete)
281 # @param [String] url the request url
292 # @param [String] url the request url
282 # @param [optional, Hash] parameters additional request parameters
293 # @param [optional, Hash] parameters additional request parameters
283 # @param [optional, Hash] options additional options
294 # @param [optional, Hash] options additional options
284 # @option options [Symbol] :success_code Successful response code (:success)
295 # @option options [Symbol] :success_code Successful response code (:success)
285 # @option options [Symbol] :failure_code Failure response code (:unauthorized)
296 # @option options [Symbol] :failure_code Failure response code (:unauthorized)
286 def self.should_allow_http_basic_auth_with_key(http_method, url, parameters={}, options={})
297 def self.should_allow_http_basic_auth_with_key(http_method, url, parameters={}, options={})
287 success_code = options[:success_code] || :success
298 success_code = options[:success_code] || :success
288 failure_code = options[:failure_code] || :unauthorized
299 failure_code = options[:failure_code] || :unauthorized
289
300
290 context "should allow http basic auth with a key for #{http_method} #{url}" do
301 context "should allow http basic auth with a key for #{http_method} #{url}" do
291 context "with a valid HTTP authentication using the API token" do
302 context "with a valid HTTP authentication using the API token" do
292 setup do
303 setup do
293 @user = User.generate_with_protected!(:admin => true)
304 @user = User.generate_with_protected!(:admin => true)
294 @token = Token.generate!(:user => @user, :action => 'api')
305 @token = Token.generate!(:user => @user, :action => 'api')
295 @authorization = ActionController::HttpAuthentication::Basic.encode_credentials(@token.value, 'X')
306 @authorization = ActionController::HttpAuthentication::Basic.encode_credentials(@token.value, 'X')
296 send(http_method, url, parameters, {:authorization => @authorization})
307 send(http_method, url, parameters, {:authorization => @authorization})
297 end
308 end
298
309
299 should_respond_with success_code
310 should_respond_with success_code
300 should_respond_with_content_type_based_on_url(url)
311 should_respond_with_content_type_based_on_url(url)
301 should_be_a_valid_response_string_based_on_url(url)
312 should_be_a_valid_response_string_based_on_url(url)
302 should "login as the user" do
313 should "login as the user" do
303 assert_equal @user, User.current
314 assert_equal @user, User.current
304 end
315 end
305 end
316 end
306
317
307 context "with an invalid HTTP authentication" do
318 context "with an invalid HTTP authentication" do
308 setup do
319 setup do
309 @user = User.generate_with_protected!
320 @user = User.generate_with_protected!
310 @token = Token.generate!(:user => @user, :action => 'feeds')
321 @token = Token.generate!(:user => @user, :action => 'feeds')
311 @authorization = ActionController::HttpAuthentication::Basic.encode_credentials(@token.value, 'X')
322 @authorization = ActionController::HttpAuthentication::Basic.encode_credentials(@token.value, 'X')
312 send(http_method, url, parameters, {:authorization => @authorization})
323 send(http_method, url, parameters, {:authorization => @authorization})
313 end
324 end
314
325
315 should_respond_with failure_code
326 should_respond_with failure_code
316 should_respond_with_content_type_based_on_url(url)
327 should_respond_with_content_type_based_on_url(url)
317 should "not login as the user" do
328 should "not login as the user" do
318 assert_equal User.anonymous, User.current
329 assert_equal User.anonymous, User.current
319 end
330 end
320 end
331 end
321 end
332 end
322 end
333 end
323
334
324 # Test that a request allows full key authentication
335 # Test that a request allows full key authentication
325 #
336 #
326 # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete)
337 # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete)
327 # @param [String] url the request url, without the key=ZXY parameter
338 # @param [String] url the request url, without the key=ZXY parameter
328 # @param [optional, Hash] parameters additional request parameters
339 # @param [optional, Hash] parameters additional request parameters
329 # @param [optional, Hash] options additional options
340 # @param [optional, Hash] options additional options
330 # @option options [Symbol] :success_code Successful response code (:success)
341 # @option options [Symbol] :success_code Successful response code (:success)
331 # @option options [Symbol] :failure_code Failure response code (:unauthorized)
342 # @option options [Symbol] :failure_code Failure response code (:unauthorized)
332 def self.should_allow_key_based_auth(http_method, url, parameters={}, options={})
343 def self.should_allow_key_based_auth(http_method, url, parameters={}, options={})
333 success_code = options[:success_code] || :success
344 success_code = options[:success_code] || :success
334 failure_code = options[:failure_code] || :unauthorized
345 failure_code = options[:failure_code] || :unauthorized
335
346
336 context "should allow key based auth using key=X for #{http_method} #{url}" do
347 context "should allow key based auth using key=X for #{http_method} #{url}" do
337 context "with a valid api token" do
348 context "with a valid api token" do
338 setup do
349 setup do
339 @user = User.generate_with_protected!(:admin => true)
350 @user = User.generate_with_protected!(:admin => true)
340 @token = Token.generate!(:user => @user, :action => 'api')
351 @token = Token.generate!(:user => @user, :action => 'api')
341 # Simple url parse to add on ?key= or &key=
352 # Simple url parse to add on ?key= or &key=
342 request_url = if url.match(/\?/)
353 request_url = if url.match(/\?/)
343 url + "&key=#{@token.value}"
354 url + "&key=#{@token.value}"
344 else
355 else
345 url + "?key=#{@token.value}"
356 url + "?key=#{@token.value}"
346 end
357 end
347 send(http_method, request_url, parameters)
358 send(http_method, request_url, parameters)
348 end
359 end
349
360
350 should_respond_with success_code
361 should_respond_with success_code
351 should_respond_with_content_type_based_on_url(url)
362 should_respond_with_content_type_based_on_url(url)
352 should_be_a_valid_response_string_based_on_url(url)
363 should_be_a_valid_response_string_based_on_url(url)
353 should "login as the user" do
364 should "login as the user" do
354 assert_equal @user, User.current
365 assert_equal @user, User.current
355 end
366 end
356 end
367 end
357
368
358 context "with an invalid api token" do
369 context "with an invalid api token" do
359 setup do
370 setup do
360 @user = User.generate_with_protected!
371 @user = User.generate_with_protected!
361 @token = Token.generate!(:user => @user, :action => 'feeds')
372 @token = Token.generate!(:user => @user, :action => 'feeds')
362 # Simple url parse to add on ?key= or &key=
373 # Simple url parse to add on ?key= or &key=
363 request_url = if url.match(/\?/)
374 request_url = if url.match(/\?/)
364 url + "&key=#{@token.value}"
375 url + "&key=#{@token.value}"
365 else
376 else
366 url + "?key=#{@token.value}"
377 url + "?key=#{@token.value}"
367 end
378 end
368 send(http_method, request_url, parameters)
379 send(http_method, request_url, parameters)
369 end
380 end
370
381
371 should_respond_with failure_code
382 should_respond_with failure_code
372 should_respond_with_content_type_based_on_url(url)
383 should_respond_with_content_type_based_on_url(url)
373 should "not login as the user" do
384 should "not login as the user" do
374 assert_equal User.anonymous, User.current
385 assert_equal User.anonymous, User.current
375 end
386 end
376 end
387 end
377 end
388 end
378
389
379 context "should allow key based auth using X-Redmine-API-Key header for #{http_method} #{url}" do
390 context "should allow key based auth using X-Redmine-API-Key header for #{http_method} #{url}" do
380 setup do
391 setup do
381 @user = User.generate_with_protected!(:admin => true)
392 @user = User.generate_with_protected!(:admin => true)
382 @token = Token.generate!(:user => @user, :action => 'api')
393 @token = Token.generate!(:user => @user, :action => 'api')
383 send(http_method, url, parameters, {'X-Redmine-API-Key' => @token.value.to_s})
394 send(http_method, url, parameters, {'X-Redmine-API-Key' => @token.value.to_s})
384 end
395 end
385
396
386 should_respond_with success_code
397 should_respond_with success_code
387 should_respond_with_content_type_based_on_url(url)
398 should_respond_with_content_type_based_on_url(url)
388 should_be_a_valid_response_string_based_on_url(url)
399 should_be_a_valid_response_string_based_on_url(url)
389 should "login as the user" do
400 should "login as the user" do
390 assert_equal @user, User.current
401 assert_equal @user, User.current
391 end
402 end
392 end
403 end
393 end
404 end
394
405
395 # Uses should_respond_with_content_type based on what's in the url:
406 # Uses should_respond_with_content_type based on what's in the url:
396 #
407 #
397 # '/project/issues.xml' => should_respond_with_content_type :xml
408 # '/project/issues.xml' => should_respond_with_content_type :xml
398 # '/project/issues.json' => should_respond_with_content_type :json
409 # '/project/issues.json' => should_respond_with_content_type :json
399 #
410 #
400 # @param [String] url Request
411 # @param [String] url Request
401 def self.should_respond_with_content_type_based_on_url(url)
412 def self.should_respond_with_content_type_based_on_url(url)
402 case
413 case
403 when url.match(/xml/i)
414 when url.match(/xml/i)
404 should_respond_with_content_type :xml
415 should_respond_with_content_type :xml
405 when url.match(/json/i)
416 when url.match(/json/i)
406 should_respond_with_content_type :json
417 should_respond_with_content_type :json
407 else
418 else
408 raise "Unknown content type for should_respond_with_content_type_based_on_url: #{url}"
419 raise "Unknown content type for should_respond_with_content_type_based_on_url: #{url}"
409 end
420 end
410
421
411 end
422 end
412
423
413 # Uses the url to assert which format the response should be in
424 # Uses the url to assert which format the response should be in
414 #
425 #
415 # '/project/issues.xml' => should_be_a_valid_xml_string
426 # '/project/issues.xml' => should_be_a_valid_xml_string
416 # '/project/issues.json' => should_be_a_valid_json_string
427 # '/project/issues.json' => should_be_a_valid_json_string
417 #
428 #
418 # @param [String] url Request
429 # @param [String] url Request
419 def self.should_be_a_valid_response_string_based_on_url(url)
430 def self.should_be_a_valid_response_string_based_on_url(url)
420 case
431 case
421 when url.match(/xml/i)
432 when url.match(/xml/i)
422 should_be_a_valid_xml_string
433 should_be_a_valid_xml_string
423 when url.match(/json/i)
434 when url.match(/json/i)
424 should_be_a_valid_json_string
435 should_be_a_valid_json_string
425 else
436 else
426 raise "Unknown content type for should_be_a_valid_response_based_on_url: #{url}"
437 raise "Unknown content type for should_be_a_valid_response_based_on_url: #{url}"
427 end
438 end
428
439
429 end
440 end
430
441
431 # Checks that the response is a valid JSON string
442 # Checks that the response is a valid JSON string
432 def self.should_be_a_valid_json_string
443 def self.should_be_a_valid_json_string
433 should "be a valid JSON string (or empty)" do
444 should "be a valid JSON string (or empty)" do
434 assert(response.body.blank? || ActiveSupport::JSON.decode(response.body))
445 assert(response.body.blank? || ActiveSupport::JSON.decode(response.body))
435 end
446 end
436 end
447 end
437
448
438 # Checks that the response is a valid XML string
449 # Checks that the response is a valid XML string
439 def self.should_be_a_valid_xml_string
450 def self.should_be_a_valid_xml_string
440 should "be a valid XML string" do
451 should "be a valid XML string" do
441 assert REXML::Document.new(response.body)
452 assert REXML::Document.new(response.body)
442 end
453 end
443 end
454 end
444
455
445 end
456 end
446
457
447 # Simple module to "namespace" all of the API tests
458 # Simple module to "namespace" all of the API tests
448 module ApiTest
459 module ApiTest
449 end
460 end
@@ -1,801 +1,854
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, :news,
26 :boards, :messages, :news,
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_attached_images_filename_extension
133 set_tmp_attachments_directory
134 a1 = Attachment.new(
135 :container => Issue.find(1),
136 :file => mock_file_with_options({:original_filename => "testtest.JPG"}),
137 :author => User.find(1))
138 assert a1.save
139 assert_equal "testtest.JPG", a1.filename
140 assert_equal "image/jpeg", a1.content_type
141 assert a1.image?
142
143 a2 = Attachment.new(
144 :container => Issue.find(1),
145 :file => mock_file_with_options({:original_filename => "testtest.jpeg"}),
146 :author => User.find(1))
147 assert a2.save
148 assert_equal "testtest.jpeg", a2.filename
149 assert_equal "image/jpeg", a2.content_type
150 assert a2.image?
151
152 a3 = Attachment.new(
153 :container => Issue.find(1),
154 :file => mock_file_with_options({:original_filename => "testtest.JPE"}),
155 :author => User.find(1))
156 assert a3.save
157 assert_equal "testtest.JPE", a3.filename
158 assert_equal "image/jpeg", a3.content_type
159 assert a3.image?
160
161 a4 = Attachment.new(
162 :container => Issue.find(1),
163 :file => mock_file_with_options({:original_filename => "Testtest.BMP"}),
164 :author => User.find(1))
165 assert a4.save
166 assert_equal "Testtest.BMP", a4.filename
167 assert_equal "image/x-ms-bmp", a4.content_type
168 assert a4.image?
169
170 to_test = {
171 'Inline image: !testtest.jpg!' =>
172 'Inline image: <img src="/attachments/download/' + a1.id.to_s + '" alt="" />',
173 'Inline image: !testtest.jpeg!' =>
174 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '" alt="" />',
175 'Inline image: !testtest.jpe!' =>
176 'Inline image: <img src="/attachments/download/' + a3.id.to_s + '" alt="" />',
177 'Inline image: !testtest.bmp!' =>
178 'Inline image: <img src="/attachments/download/' + a4.id.to_s + '" alt="" />',
179 }
180
181 attachments = [a1, a2, a3, a4]
182 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
183 end
184
132 def test_textile_external_links
185 def test_textile_external_links
133 to_test = {
186 to_test = {
134 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>',
187 '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>',
188 '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>',
189 '"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>',
190 '"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",
191 "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
192 # 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",
193 "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
194 # mailto link
142 "\"system administrator\":mailto:sysadmin@example.com?subject=redmine%20permissions" => "<a href=\"mailto:sysadmin@example.com?subject=redmine%20permissions\">system administrator</a>",
195 "\"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
196 # 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>',
197 '"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
198 # escaping
146 '"test":http://foo"bar' => '<a href="http://foo&quot;bar" class="external">test</a>',
199 '"test":http://foo"bar' => '<a href="http://foo&quot;bar" class="external">test</a>',
147 }
200 }
148 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
201 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
149 end
202 end
150
203
151 def test_redmine_links
204 def test_redmine_links
152 issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3},
205 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)')
206 :class => 'issue status-1 priority-1 overdue', :title => 'Error 281 when updating a recipe (New)')
154
207
155 changeset_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
208 changeset_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
156 :class => 'changeset', :title => 'My very first commit')
209 :class => 'changeset', :title => 'My very first commit')
157 changeset_link2 = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
210 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')
211 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
159
212
160 document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
213 document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
161 :class => 'document')
214 :class => 'document')
162
215
163 version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
216 version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
164 :class => 'version')
217 :class => 'version')
165
218
166 board_url = {:controller => 'boards', :action => 'show', :id => 2, :project_id => 'ecookbook'}
219 board_url = {:controller => 'boards', :action => 'show', :id => 2, :project_id => 'ecookbook'}
167
220
168 message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4}
221 message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4}
169
222
170 news_url = {:controller => 'news', :action => 'show', :id => 1}
223 news_url = {:controller => 'news', :action => 'show', :id => 1}
171
224
172 project_url = {:controller => 'projects', :action => 'show', :id => 'subproject1'}
225 project_url = {:controller => 'projects', :action => 'show', :id => 'subproject1'}
173
226
174 source_url = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}
227 source_url = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}
175 source_url_with_ext = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file.ext']}
228 source_url_with_ext = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file.ext']}
176
229
177 to_test = {
230 to_test = {
178 # tickets
231 # tickets
179 '#3, [#3], (#3) and #3.' => "#{issue_link}, [#{issue_link}], (#{issue_link}) and #{issue_link}.",
232 '#3, [#3], (#3) and #3.' => "#{issue_link}, [#{issue_link}], (#{issue_link}) and #{issue_link}.",
180 # changesets
233 # changesets
181 'r1' => changeset_link,
234 'r1' => changeset_link,
182 'r1.' => "#{changeset_link}.",
235 'r1.' => "#{changeset_link}.",
183 'r1, r2' => "#{changeset_link}, #{changeset_link2}",
236 'r1, r2' => "#{changeset_link}, #{changeset_link2}",
184 'r1,r2' => "#{changeset_link},#{changeset_link2}",
237 'r1,r2' => "#{changeset_link},#{changeset_link2}",
185 # documents
238 # documents
186 'document#1' => document_link,
239 'document#1' => document_link,
187 'document:"Test document"' => document_link,
240 'document:"Test document"' => document_link,
188 # versions
241 # versions
189 'version#2' => version_link,
242 'version#2' => version_link,
190 'version:1.0' => version_link,
243 'version:1.0' => version_link,
191 'version:"1.0"' => version_link,
244 'version:"1.0"' => version_link,
192 # source
245 # source
193 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
246 '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') + ".",
247 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".",
195 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
248 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
196 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".",
249 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".",
197 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
250 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
198 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",",
251 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",",
199 'source:/some/file@52' => link_to('source:/some/file@52', source_url.merge(:rev => 52), :class => 'source'),
252 'source:/some/file@52' => link_to('source:/some/file@52', source_url.merge(:rev => 52), :class => 'source'),
200 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_ext.merge(:rev => 52), :class => 'source'),
253 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_ext.merge(:rev => 52), :class => 'source'),
201 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url.merge(:anchor => 'L110'), :class => 'source'),
254 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url.merge(:anchor => 'L110'), :class => 'source'),
202 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext.merge(:anchor => 'L110'), :class => 'source'),
255 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext.merge(:anchor => 'L110'), :class => 'source'),
203 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url.merge(:rev => 52, :anchor => 'L110'), :class => 'source'),
256 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url.merge(:rev => 52, :anchor => 'L110'), :class => 'source'),
204 'export:/some/file' => link_to('export:/some/file', source_url.merge(:format => 'raw'), :class => 'source download'),
257 'export:/some/file' => link_to('export:/some/file', source_url.merge(:format => 'raw'), :class => 'source download'),
205 # forum
258 # forum
206 'forum#2' => link_to('Discussion', board_url, :class => 'board'),
259 'forum#2' => link_to('Discussion', board_url, :class => 'board'),
207 'forum:Discussion' => link_to('Discussion', board_url, :class => 'board'),
260 'forum:Discussion' => link_to('Discussion', board_url, :class => 'board'),
208 # message
261 # message
209 'message#4' => link_to('Post 2', message_url, :class => 'message'),
262 'message#4' => link_to('Post 2', message_url, :class => 'message'),
210 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5', :r => 5), :class => 'message'),
263 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5', :r => 5), :class => 'message'),
211 # news
264 # news
212 'news#1' => link_to('eCookbook first release !', news_url, :class => 'news'),
265 'news#1' => link_to('eCookbook first release !', news_url, :class => 'news'),
213 'news:"eCookbook first release !"' => link_to('eCookbook first release !', news_url, :class => 'news'),
266 'news:"eCookbook first release !"' => link_to('eCookbook first release !', news_url, :class => 'news'),
214 # project
267 # project
215 'project#3' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
268 'project#3' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
216 'project:subproject1' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
269 'project:subproject1' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
217 'project:"eCookbook subProject 1"' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
270 'project:"eCookbook subProject 1"' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
218 # escaping
271 # escaping
219 '!#3.' => '#3.',
272 '!#3.' => '#3.',
220 '!r1' => 'r1',
273 '!r1' => 'r1',
221 '!document#1' => 'document#1',
274 '!document#1' => 'document#1',
222 '!document:"Test document"' => 'document:"Test document"',
275 '!document:"Test document"' => 'document:"Test document"',
223 '!version#2' => 'version#2',
276 '!version#2' => 'version#2',
224 '!version:1.0' => 'version:1.0',
277 '!version:1.0' => 'version:1.0',
225 '!version:"1.0"' => 'version:"1.0"',
278 '!version:"1.0"' => 'version:"1.0"',
226 '!source:/some/file' => 'source:/some/file',
279 '!source:/some/file' => 'source:/some/file',
227 # not found
280 # not found
228 '#0123456789' => '#0123456789',
281 '#0123456789' => '#0123456789',
229 # invalid expressions
282 # invalid expressions
230 'source:' => 'source:',
283 'source:' => 'source:',
231 # url hash
284 # url hash
232 "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
285 "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
233 }
286 }
234 @project = Project.find(1)
287 @project = Project.find(1)
235 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
288 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
236 end
289 end
237
290
238 def test_cross_project_redmine_links
291 def test_cross_project_redmine_links
239 source_link = link_to('ecookbook:source:/some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']},
292 source_link = link_to('ecookbook:source:/some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']},
240 :class => 'source')
293 :class => 'source')
241
294
242 changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
295 changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
243 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
296 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
244
297
245 to_test = {
298 to_test = {
246 # documents
299 # documents
247 'document:"Test document"' => 'document:"Test document"',
300 'document:"Test document"' => 'document:"Test document"',
248 'ecookbook:document:"Test document"' => '<a href="/documents/1" class="document">Test document</a>',
301 'ecookbook:document:"Test document"' => '<a href="/documents/1" class="document">Test document</a>',
249 'invalid:document:"Test document"' => 'invalid:document:"Test document"',
302 'invalid:document:"Test document"' => 'invalid:document:"Test document"',
250 # versions
303 # versions
251 'version:"1.0"' => 'version:"1.0"',
304 'version:"1.0"' => 'version:"1.0"',
252 'ecookbook:version:"1.0"' => '<a href="/versions/2" class="version">1.0</a>',
305 'ecookbook:version:"1.0"' => '<a href="/versions/2" class="version">1.0</a>',
253 'invalid:version:"1.0"' => 'invalid:version:"1.0"',
306 'invalid:version:"1.0"' => 'invalid:version:"1.0"',
254 # changeset
307 # changeset
255 'r2' => 'r2',
308 'r2' => 'r2',
256 'ecookbook:r2' => changeset_link,
309 'ecookbook:r2' => changeset_link,
257 'invalid:r2' => 'invalid:r2',
310 'invalid:r2' => 'invalid:r2',
258 # source
311 # source
259 'source:/some/file' => 'source:/some/file',
312 'source:/some/file' => 'source:/some/file',
260 'ecookbook:source:/some/file' => source_link,
313 'ecookbook:source:/some/file' => source_link,
261 'invalid:source:/some/file' => 'invalid:source:/some/file',
314 'invalid:source:/some/file' => 'invalid:source:/some/file',
262 }
315 }
263 @project = Project.find(3)
316 @project = Project.find(3)
264 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
317 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
265 end
318 end
266
319
267 def test_redmine_links_git_commit
320 def test_redmine_links_git_commit
268 changeset_link = link_to('abcd',
321 changeset_link = link_to('abcd',
269 {
322 {
270 :controller => 'repositories',
323 :controller => 'repositories',
271 :action => 'revision',
324 :action => 'revision',
272 :id => 'subproject1',
325 :id => 'subproject1',
273 :rev => 'abcd',
326 :rev => 'abcd',
274 },
327 },
275 :class => 'changeset', :title => 'test commit')
328 :class => 'changeset', :title => 'test commit')
276 to_test = {
329 to_test = {
277 'commit:abcd' => changeset_link,
330 'commit:abcd' => changeset_link,
278 }
331 }
279 @project = Project.find(3)
332 @project = Project.find(3)
280 r = Repository::Git.create!(:project => @project, :url => '/tmp/test/git')
333 r = Repository::Git.create!(:project => @project, :url => '/tmp/test/git')
281 assert r
334 assert r
282 c = Changeset.new(:repository => r,
335 c = Changeset.new(:repository => r,
283 :committed_on => Time.now,
336 :committed_on => Time.now,
284 :revision => 'abcd',
337 :revision => 'abcd',
285 :scmid => 'abcd',
338 :scmid => 'abcd',
286 :comments => 'test commit')
339 :comments => 'test commit')
287 assert( c.save )
340 assert( c.save )
288 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
341 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
289 end
342 end
290
343
291 # TODO: Bazaar commit id contains mail address, so it contains '@' and '_'.
344 # TODO: Bazaar commit id contains mail address, so it contains '@' and '_'.
292 def test_redmine_links_darcs_commit
345 def test_redmine_links_darcs_commit
293 changeset_link = link_to('20080308225258-98289-abcd456efg.gz',
346 changeset_link = link_to('20080308225258-98289-abcd456efg.gz',
294 {
347 {
295 :controller => 'repositories',
348 :controller => 'repositories',
296 :action => 'revision',
349 :action => 'revision',
297 :id => 'subproject1',
350 :id => 'subproject1',
298 :rev => '123',
351 :rev => '123',
299 },
352 },
300 :class => 'changeset', :title => 'test commit')
353 :class => 'changeset', :title => 'test commit')
301 to_test = {
354 to_test = {
302 'commit:20080308225258-98289-abcd456efg.gz' => changeset_link,
355 'commit:20080308225258-98289-abcd456efg.gz' => changeset_link,
303 }
356 }
304 @project = Project.find(3)
357 @project = Project.find(3)
305 r = Repository::Darcs.create!(
358 r = Repository::Darcs.create!(
306 :project => @project, :url => '/tmp/test/darcs',
359 :project => @project, :url => '/tmp/test/darcs',
307 :log_encoding => 'UTF-8')
360 :log_encoding => 'UTF-8')
308 assert r
361 assert r
309 c = Changeset.new(:repository => r,
362 c = Changeset.new(:repository => r,
310 :committed_on => Time.now,
363 :committed_on => Time.now,
311 :revision => '123',
364 :revision => '123',
312 :scmid => '20080308225258-98289-abcd456efg.gz',
365 :scmid => '20080308225258-98289-abcd456efg.gz',
313 :comments => 'test commit')
366 :comments => 'test commit')
314 assert( c.save )
367 assert( c.save )
315 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
368 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
316 end
369 end
317
370
318 def test_redmine_links_mercurial_commit
371 def test_redmine_links_mercurial_commit
319 changeset_link_rev = link_to('r123',
372 changeset_link_rev = link_to('r123',
320 {
373 {
321 :controller => 'repositories',
374 :controller => 'repositories',
322 :action => 'revision',
375 :action => 'revision',
323 :id => 'subproject1',
376 :id => 'subproject1',
324 :rev => '123' ,
377 :rev => '123' ,
325 },
378 },
326 :class => 'changeset', :title => 'test commit')
379 :class => 'changeset', :title => 'test commit')
327 changeset_link_commit = link_to('abcd',
380 changeset_link_commit = link_to('abcd',
328 {
381 {
329 :controller => 'repositories',
382 :controller => 'repositories',
330 :action => 'revision',
383 :action => 'revision',
331 :id => 'subproject1',
384 :id => 'subproject1',
332 :rev => 'abcd' ,
385 :rev => 'abcd' ,
333 },
386 },
334 :class => 'changeset', :title => 'test commit')
387 :class => 'changeset', :title => 'test commit')
335 to_test = {
388 to_test = {
336 'r123' => changeset_link_rev,
389 'r123' => changeset_link_rev,
337 'commit:abcd' => changeset_link_commit,
390 'commit:abcd' => changeset_link_commit,
338 }
391 }
339 @project = Project.find(3)
392 @project = Project.find(3)
340 r = Repository::Mercurial.create!(:project => @project, :url => '/tmp/test')
393 r = Repository::Mercurial.create!(:project => @project, :url => '/tmp/test')
341 assert r
394 assert r
342 c = Changeset.new(:repository => r,
395 c = Changeset.new(:repository => r,
343 :committed_on => Time.now,
396 :committed_on => Time.now,
344 :revision => '123',
397 :revision => '123',
345 :scmid => 'abcd',
398 :scmid => 'abcd',
346 :comments => 'test commit')
399 :comments => 'test commit')
347 assert( c.save )
400 assert( c.save )
348 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
401 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
349 end
402 end
350
403
351 def test_attachment_links
404 def test_attachment_links
352 attachment_link = link_to('error281.txt', {:controller => 'attachments', :action => 'download', :id => '1'}, :class => 'attachment')
405 attachment_link = link_to('error281.txt', {:controller => 'attachments', :action => 'download', :id => '1'}, :class => 'attachment')
353 to_test = {
406 to_test = {
354 'attachment:error281.txt' => attachment_link
407 'attachment:error281.txt' => attachment_link
355 }
408 }
356 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => Issue.find(3).attachments), "#{text} failed" }
409 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => Issue.find(3).attachments), "#{text} failed" }
357 end
410 end
358
411
359 def test_wiki_links
412 def test_wiki_links
360 to_test = {
413 to_test = {
361 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
414 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
362 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
415 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
363 # title content should be formatted
416 # title content should be formatted
364 '[[Another page|With _styled_ *title*]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">With <em>styled</em> <strong>title</strong></a>',
417 '[[Another page|With _styled_ *title*]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">With <em>styled</em> <strong>title</strong></a>',
365 '[[Another page|With title containing <strong>HTML entities &amp; markups</strong>]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">With title containing &lt;strong&gt;HTML entities &amp; markups&lt;/strong&gt;</a>',
418 '[[Another page|With title containing <strong>HTML entities &amp; markups</strong>]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">With title containing &lt;strong&gt;HTML entities &amp; markups&lt;/strong&gt;</a>',
366 # link with anchor
419 # link with anchor
367 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
420 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
368 '[[Another page#anchor|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page#anchor" class="wiki-page">Page</a>',
421 '[[Another page#anchor|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page#anchor" class="wiki-page">Page</a>',
369 # page that doesn't exist
422 # page that doesn't exist
370 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
423 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
371 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">404</a>',
424 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">404</a>',
372 # link to another project wiki
425 # link to another project wiki
373 '[[onlinestore:]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">onlinestore</a>',
426 '[[onlinestore:]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">onlinestore</a>',
374 '[[onlinestore:|Wiki]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">Wiki</a>',
427 '[[onlinestore:|Wiki]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">Wiki</a>',
375 '[[onlinestore:Start page]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Start page</a>',
428 '[[onlinestore:Start page]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Start page</a>',
376 '[[onlinestore:Start page|Text]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Text</a>',
429 '[[onlinestore:Start page|Text]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Text</a>',
377 '[[onlinestore:Unknown page]]' => '<a href="/projects/onlinestore/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
430 '[[onlinestore:Unknown page]]' => '<a href="/projects/onlinestore/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
378 # striked through link
431 # striked through link
379 '-[[Another page|Page]]-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a></del>',
432 '-[[Another page|Page]]-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a></del>',
380 '-[[Another page|Page]] link-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a> link</del>',
433 '-[[Another page|Page]] link-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a> link</del>',
381 # escaping
434 # escaping
382 '![[Another page|Page]]' => '[[Another page|Page]]',
435 '![[Another page|Page]]' => '[[Another page|Page]]',
383 # project does not exist
436 # project does not exist
384 '[[unknowproject:Start]]' => '[[unknowproject:Start]]',
437 '[[unknowproject:Start]]' => '[[unknowproject:Start]]',
385 '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]',
438 '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]',
386 }
439 }
387
440
388 @project = Project.find(1)
441 @project = Project.find(1)
389 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
442 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
390 end
443 end
391
444
392 def test_wiki_links_within_local_file_generation_context
445 def test_wiki_links_within_local_file_generation_context
393
446
394 to_test = {
447 to_test = {
395 # link to a page
448 # link to a page
396 '[[CookBook documentation]]' => '<a href="CookBook_documentation.html" class="wiki-page">CookBook documentation</a>',
449 '[[CookBook documentation]]' => '<a href="CookBook_documentation.html" class="wiki-page">CookBook documentation</a>',
397 '[[CookBook documentation|documentation]]' => '<a href="CookBook_documentation.html" class="wiki-page">documentation</a>',
450 '[[CookBook documentation|documentation]]' => '<a href="CookBook_documentation.html" class="wiki-page">documentation</a>',
398 '[[CookBook documentation#One-section]]' => '<a href="CookBook_documentation.html#One-section" class="wiki-page">CookBook documentation</a>',
451 '[[CookBook documentation#One-section]]' => '<a href="CookBook_documentation.html#One-section" class="wiki-page">CookBook documentation</a>',
399 '[[CookBook documentation#One-section|documentation]]' => '<a href="CookBook_documentation.html#One-section" class="wiki-page">documentation</a>',
452 '[[CookBook documentation#One-section|documentation]]' => '<a href="CookBook_documentation.html#One-section" class="wiki-page">documentation</a>',
400 # page that doesn't exist
453 # page that doesn't exist
401 '[[Unknown page]]' => '<a href="Unknown_page.html" class="wiki-page new">Unknown page</a>',
454 '[[Unknown page]]' => '<a href="Unknown_page.html" class="wiki-page new">Unknown page</a>',
402 '[[Unknown page|404]]' => '<a href="Unknown_page.html" class="wiki-page new">404</a>',
455 '[[Unknown page|404]]' => '<a href="Unknown_page.html" class="wiki-page new">404</a>',
403 '[[Unknown page#anchor]]' => '<a href="Unknown_page.html#anchor" class="wiki-page new">Unknown page</a>',
456 '[[Unknown page#anchor]]' => '<a href="Unknown_page.html#anchor" class="wiki-page new">Unknown page</a>',
404 '[[Unknown page#anchor|404]]' => '<a href="Unknown_page.html#anchor" class="wiki-page new">404</a>',
457 '[[Unknown page#anchor|404]]' => '<a href="Unknown_page.html#anchor" class="wiki-page new">404</a>',
405 }
458 }
406
459
407 @project = Project.find(1)
460 @project = Project.find(1)
408
461
409 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :local) }
462 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :local) }
410 end
463 end
411
464
412 def test_html_tags
465 def test_html_tags
413 to_test = {
466 to_test = {
414 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
467 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
415 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
468 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
416 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
469 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
417 # do not escape pre/code tags
470 # do not escape pre/code tags
418 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
471 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
419 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
472 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
420 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
473 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
421 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
474 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
422 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
475 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
423 # remove attributes except class
476 # remove attributes except class
424 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
477 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
425 '<pre class="foo">some text</pre>' => '<pre class="foo">some text</pre>',
478 '<pre class="foo">some text</pre>' => '<pre class="foo">some text</pre>',
426 "<pre class='foo bar'>some text</pre>" => "<pre class='foo bar'>some text</pre>",
479 "<pre class='foo bar'>some text</pre>" => "<pre class='foo bar'>some text</pre>",
427 '<pre class="foo bar">some text</pre>' => '<pre class="foo bar">some text</pre>',
480 '<pre class="foo bar">some text</pre>' => '<pre class="foo bar">some text</pre>',
428 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
481 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
429 # xss
482 # xss
430 '<pre><code class=""onmouseover="alert(1)">text</code></pre>' => '<pre><code>text</code></pre>',
483 '<pre><code class=""onmouseover="alert(1)">text</code></pre>' => '<pre><code>text</code></pre>',
431 '<pre class=""onmouseover="alert(1)">text</pre>' => '<pre>text</pre>',
484 '<pre class=""onmouseover="alert(1)">text</pre>' => '<pre>text</pre>',
432 }
485 }
433 to_test.each { |text, result| assert_equal result, textilizable(text) }
486 to_test.each { |text, result| assert_equal result, textilizable(text) }
434 end
487 end
435
488
436 def test_allowed_html_tags
489 def test_allowed_html_tags
437 to_test = {
490 to_test = {
438 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
491 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
439 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
492 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
440 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
493 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
441 }
494 }
442 to_test.each { |text, result| assert_equal result, textilizable(text) }
495 to_test.each { |text, result| assert_equal result, textilizable(text) }
443 end
496 end
444
497
445 def test_pre_tags
498 def test_pre_tags
446 raw = <<-RAW
499 raw = <<-RAW
447 Before
500 Before
448
501
449 <pre>
502 <pre>
450 <prepared-statement-cache-size>32</prepared-statement-cache-size>
503 <prepared-statement-cache-size>32</prepared-statement-cache-size>
451 </pre>
504 </pre>
452
505
453 After
506 After
454 RAW
507 RAW
455
508
456 expected = <<-EXPECTED
509 expected = <<-EXPECTED
457 <p>Before</p>
510 <p>Before</p>
458 <pre>
511 <pre>
459 &lt;prepared-statement-cache-size&gt;32&lt;/prepared-statement-cache-size&gt;
512 &lt;prepared-statement-cache-size&gt;32&lt;/prepared-statement-cache-size&gt;
460 </pre>
513 </pre>
461 <p>After</p>
514 <p>After</p>
462 EXPECTED
515 EXPECTED
463
516
464 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
517 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
465 end
518 end
466
519
467 def test_pre_content_should_not_parse_wiki_and_redmine_links
520 def test_pre_content_should_not_parse_wiki_and_redmine_links
468 raw = <<-RAW
521 raw = <<-RAW
469 [[CookBook documentation]]
522 [[CookBook documentation]]
470
523
471 #1
524 #1
472
525
473 <pre>
526 <pre>
474 [[CookBook documentation]]
527 [[CookBook documentation]]
475
528
476 #1
529 #1
477 </pre>
530 </pre>
478 RAW
531 RAW
479
532
480 expected = <<-EXPECTED
533 expected = <<-EXPECTED
481 <p><a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a></p>
534 <p><a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a></p>
482 <p><a href="/issues/1" class="issue status-1 priority-1" title="Can't print recipes (New)">#1</a></p>
535 <p><a href="/issues/1" class="issue status-1 priority-1" title="Can't print recipes (New)">#1</a></p>
483 <pre>
536 <pre>
484 [[CookBook documentation]]
537 [[CookBook documentation]]
485
538
486 #1
539 #1
487 </pre>
540 </pre>
488 EXPECTED
541 EXPECTED
489
542
490 @project = Project.find(1)
543 @project = Project.find(1)
491 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
544 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
492 end
545 end
493
546
494 def test_non_closing_pre_blocks_should_be_closed
547 def test_non_closing_pre_blocks_should_be_closed
495 raw = <<-RAW
548 raw = <<-RAW
496 <pre><code>
549 <pre><code>
497 RAW
550 RAW
498
551
499 expected = <<-EXPECTED
552 expected = <<-EXPECTED
500 <pre><code>
553 <pre><code>
501 </code></pre>
554 </code></pre>
502 EXPECTED
555 EXPECTED
503
556
504 @project = Project.find(1)
557 @project = Project.find(1)
505 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
558 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
506 end
559 end
507
560
508 def test_syntax_highlight
561 def test_syntax_highlight
509 raw = <<-RAW
562 raw = <<-RAW
510 <pre><code class="ruby">
563 <pre><code class="ruby">
511 # Some ruby code here
564 # Some ruby code here
512 </code></pre>
565 </code></pre>
513 RAW
566 RAW
514
567
515 expected = <<-EXPECTED
568 expected = <<-EXPECTED
516 <pre><code class="ruby syntaxhl"><span class=\"CodeRay\"><span class="line-numbers">1</span><span class="comment"># Some ruby code here</span></span>
569 <pre><code class="ruby syntaxhl"><span class=\"CodeRay\"><span class="line-numbers">1</span><span class="comment"># Some ruby code here</span></span>
517 </code></pre>
570 </code></pre>
518 EXPECTED
571 EXPECTED
519
572
520 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
573 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
521 end
574 end
522
575
523 def test_wiki_links_in_tables
576 def test_wiki_links_in_tables
524 to_test = {"|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" =>
577 to_test = {"|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" =>
525 '<tr><td><a href="/projects/ecookbook/wiki/Page" class="wiki-page new">Link title</a></td>' +
578 '<tr><td><a href="/projects/ecookbook/wiki/Page" class="wiki-page new">Link title</a></td>' +
526 '<td><a href="/projects/ecookbook/wiki/Other_Page" class="wiki-page new">Other title</a></td>' +
579 '<td><a href="/projects/ecookbook/wiki/Other_Page" class="wiki-page new">Other title</a></td>' +
527 '</tr><tr><td>Cell 21</td><td><a href="/projects/ecookbook/wiki/Last_page" class="wiki-page new">Last page</a></td></tr>'
580 '</tr><tr><td>Cell 21</td><td><a href="/projects/ecookbook/wiki/Last_page" class="wiki-page new">Last page</a></td></tr>'
528 }
581 }
529 @project = Project.find(1)
582 @project = Project.find(1)
530 to_test.each { |text, result| assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '') }
583 to_test.each { |text, result| assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '') }
531 end
584 end
532
585
533 def test_text_formatting
586 def test_text_formatting
534 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
587 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
535 '(_text within parentheses_)' => '(<em>text within parentheses</em>)',
588 '(_text within parentheses_)' => '(<em>text within parentheses</em>)',
536 'a *Humane Web* Text Generator' => 'a <strong>Humane Web</strong> Text Generator',
589 'a *Humane Web* Text Generator' => 'a <strong>Humane Web</strong> Text Generator',
537 '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>',
590 '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>',
538 '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',
591 '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',
539 }
592 }
540 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
593 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
541 end
594 end
542
595
543 def test_wiki_horizontal_rule
596 def test_wiki_horizontal_rule
544 assert_equal '<hr />', textilizable('---')
597 assert_equal '<hr />', textilizable('---')
545 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
598 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
546 end
599 end
547
600
548 def test_footnotes
601 def test_footnotes
549 raw = <<-RAW
602 raw = <<-RAW
550 This is some text[1].
603 This is some text[1].
551
604
552 fn1. This is the foot note
605 fn1. This is the foot note
553 RAW
606 RAW
554
607
555 expected = <<-EXPECTED
608 expected = <<-EXPECTED
556 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
609 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
557 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
610 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
558 EXPECTED
611 EXPECTED
559
612
560 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
613 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
561 end
614 end
562
615
563 def test_headings
616 def test_headings
564 raw = 'h1. Some heading'
617 raw = 'h1. Some heading'
565 expected = %|<a name="Some-heading"></a>\n<h1 >Some heading<a href="#Some-heading" class="wiki-anchor">&para;</a></h1>|
618 expected = %|<a name="Some-heading"></a>\n<h1 >Some heading<a href="#Some-heading" class="wiki-anchor">&para;</a></h1>|
566
619
567 assert_equal expected, textilizable(raw)
620 assert_equal expected, textilizable(raw)
568 end
621 end
569
622
570 def test_headings_with_special_chars
623 def test_headings_with_special_chars
571 # This test makes sure that the generated anchor names match the expected
624 # This test makes sure that the generated anchor names match the expected
572 # ones even if the heading text contains unconventional characters
625 # ones even if the heading text contains unconventional characters
573 raw = 'h1. Some heading related to version 0.5'
626 raw = 'h1. Some heading related to version 0.5'
574 anchor = sanitize_anchor_name("Some-heading-related-to-version-0.5")
627 anchor = sanitize_anchor_name("Some-heading-related-to-version-0.5")
575 expected = %|<a name="#{anchor}"></a>\n<h1 >Some heading related to version 0.5<a href="##{anchor}" class="wiki-anchor">&para;</a></h1>|
628 expected = %|<a name="#{anchor}"></a>\n<h1 >Some heading related to version 0.5<a href="##{anchor}" class="wiki-anchor">&para;</a></h1>|
576
629
577 assert_equal expected, textilizable(raw)
630 assert_equal expected, textilizable(raw)
578 end
631 end
579
632
580 def test_wiki_links_within_wiki_page_context
633 def test_wiki_links_within_wiki_page_context
581
634
582 page = WikiPage.find_by_title('Another_page' )
635 page = WikiPage.find_by_title('Another_page' )
583
636
584 to_test = {
637 to_test = {
585 # link to another page
638 # link to another page
586 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
639 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
587 '[[CookBook documentation|documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">documentation</a>',
640 '[[CookBook documentation|documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">documentation</a>',
588 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
641 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
589 '[[CookBook documentation#One-section|documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">documentation</a>',
642 '[[CookBook documentation#One-section|documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">documentation</a>',
590 # link to the current page
643 # link to the current page
591 '[[Another page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Another page</a>',
644 '[[Another page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Another page</a>',
592 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
645 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
593 '[[Another page#anchor]]' => '<a href="#anchor" class="wiki-page">Another page</a>',
646 '[[Another page#anchor]]' => '<a href="#anchor" class="wiki-page">Another page</a>',
594 '[[Another page#anchor|Page]]' => '<a href="#anchor" class="wiki-page">Page</a>',
647 '[[Another page#anchor|Page]]' => '<a href="#anchor" class="wiki-page">Page</a>',
595 # page that doesn't exist
648 # page that doesn't exist
596 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
649 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
597 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">404</a>',
650 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">404</a>',
598 '[[Unknown page#anchor]]' => '<a href="/projects/ecookbook/wiki/Unknown_page#anchor" class="wiki-page new">Unknown page</a>',
651 '[[Unknown page#anchor]]' => '<a href="/projects/ecookbook/wiki/Unknown_page#anchor" class="wiki-page new">Unknown page</a>',
599 '[[Unknown page#anchor|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page#anchor" class="wiki-page new">404</a>',
652 '[[Unknown page#anchor|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page#anchor" class="wiki-page new">404</a>',
600 }
653 }
601
654
602 @project = Project.find(1)
655 @project = Project.find(1)
603
656
604 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(WikiContent.generate!( :text => text, :page => page ), :text) }
657 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(WikiContent.generate!( :text => text, :page => page ), :text) }
605 end
658 end
606
659
607 def test_wiki_links_anchor_option_should_prepend_page_title_to_href
660 def test_wiki_links_anchor_option_should_prepend_page_title_to_href
608
661
609 to_test = {
662 to_test = {
610 # link to a page
663 # link to a page
611 '[[CookBook documentation]]' => '<a href="#CookBook_documentation" class="wiki-page">CookBook documentation</a>',
664 '[[CookBook documentation]]' => '<a href="#CookBook_documentation" class="wiki-page">CookBook documentation</a>',
612 '[[CookBook documentation|documentation]]' => '<a href="#CookBook_documentation" class="wiki-page">documentation</a>',
665 '[[CookBook documentation|documentation]]' => '<a href="#CookBook_documentation" class="wiki-page">documentation</a>',
613 '[[CookBook documentation#One-section]]' => '<a href="#CookBook_documentation_One-section" class="wiki-page">CookBook documentation</a>',
666 '[[CookBook documentation#One-section]]' => '<a href="#CookBook_documentation_One-section" class="wiki-page">CookBook documentation</a>',
614 '[[CookBook documentation#One-section|documentation]]' => '<a href="#CookBook_documentation_One-section" class="wiki-page">documentation</a>',
667 '[[CookBook documentation#One-section|documentation]]' => '<a href="#CookBook_documentation_One-section" class="wiki-page">documentation</a>',
615 # page that doesn't exist
668 # page that doesn't exist
616 '[[Unknown page]]' => '<a href="#Unknown_page" class="wiki-page new">Unknown page</a>',
669 '[[Unknown page]]' => '<a href="#Unknown_page" class="wiki-page new">Unknown page</a>',
617 '[[Unknown page|404]]' => '<a href="#Unknown_page" class="wiki-page new">404</a>',
670 '[[Unknown page|404]]' => '<a href="#Unknown_page" class="wiki-page new">404</a>',
618 '[[Unknown page#anchor]]' => '<a href="#Unknown_page_anchor" class="wiki-page new">Unknown page</a>',
671 '[[Unknown page#anchor]]' => '<a href="#Unknown_page_anchor" class="wiki-page new">Unknown page</a>',
619 '[[Unknown page#anchor|404]]' => '<a href="#Unknown_page_anchor" class="wiki-page new">404</a>',
672 '[[Unknown page#anchor|404]]' => '<a href="#Unknown_page_anchor" class="wiki-page new">404</a>',
620 }
673 }
621
674
622 @project = Project.find(1)
675 @project = Project.find(1)
623
676
624 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :anchor) }
677 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :anchor) }
625 end
678 end
626
679
627 def test_headings_in_wiki_single_page_export_should_be_prepended_with_page_title
680 def test_headings_in_wiki_single_page_export_should_be_prepended_with_page_title
628 page = WikiPage.generate!( :title => 'Page Title' )
681 page = WikiPage.generate!( :title => 'Page Title' )
629 content = WikiContent.generate!( :text => 'h1. Some heading', :page => page )
682 content = WikiContent.generate!( :text => 'h1. Some heading', :page => page )
630
683
631 expected = %|<a name="Page_Title_Some-heading"></a>\n<h1 >Some heading<a href="#Page_Title_Some-heading" class="wiki-anchor">&para;</a></h1>|
684 expected = %|<a name="Page_Title_Some-heading"></a>\n<h1 >Some heading<a href="#Page_Title_Some-heading" class="wiki-anchor">&para;</a></h1>|
632
685
633 assert_equal expected, textilizable(content, :text, :wiki_links => :anchor )
686 assert_equal expected, textilizable(content, :text, :wiki_links => :anchor )
634 end
687 end
635
688
636 def test_table_of_content
689 def test_table_of_content
637 raw = <<-RAW
690 raw = <<-RAW
638 {{toc}}
691 {{toc}}
639
692
640 h1. Title
693 h1. Title
641
694
642 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
695 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
643
696
644 h2. Subtitle with a [[Wiki]] link
697 h2. Subtitle with a [[Wiki]] link
645
698
646 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
699 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
647
700
648 h2. Subtitle with [[Wiki|another Wiki]] link
701 h2. Subtitle with [[Wiki|another Wiki]] link
649
702
650 h2. Subtitle with %{color:red}red text%
703 h2. Subtitle with %{color:red}red text%
651
704
652 <pre>
705 <pre>
653 some code
706 some code
654 </pre>
707 </pre>
655
708
656 h3. Subtitle with *some* _modifiers_
709 h3. Subtitle with *some* _modifiers_
657
710
658 h1. Another title
711 h1. Another title
659
712
660 h3. An "Internet link":http://www.redmine.org/ inside subtitle
713 h3. An "Internet link":http://www.redmine.org/ inside subtitle
661
714
662 h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
715 h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
663
716
664 RAW
717 RAW
665
718
666 expected = '<ul class="toc">' +
719 expected = '<ul class="toc">' +
667 '<li><a href="#Title">Title</a>' +
720 '<li><a href="#Title">Title</a>' +
668 '<ul>' +
721 '<ul>' +
669 '<li><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
722 '<li><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
670 '<li><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
723 '<li><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
671 '<li><a href="#Subtitle-with-red-text">Subtitle with red text</a>' +
724 '<li><a href="#Subtitle-with-red-text">Subtitle with red text</a>' +
672 '<ul>' +
725 '<ul>' +
673 '<li><a href="#Subtitle-with-some-modifiers">Subtitle with some modifiers</a></li>' +
726 '<li><a href="#Subtitle-with-some-modifiers">Subtitle with some modifiers</a></li>' +
674 '</ul>' +
727 '</ul>' +
675 '</li>' +
728 '</li>' +
676 '</ul>' +
729 '</ul>' +
677 '</li>' +
730 '</li>' +
678 '<li><a href="#Another-title">Another title</a>' +
731 '<li><a href="#Another-title">Another title</a>' +
679 '<ul>' +
732 '<ul>' +
680 '<li>' +
733 '<li>' +
681 '<ul>' +
734 '<ul>' +
682 '<li><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
735 '<li><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
683 '</ul>' +
736 '</ul>' +
684 '</li>' +
737 '</li>' +
685 '<li><a href="#Project-Name">Project Name</a></li>' +
738 '<li><a href="#Project-Name">Project Name</a></li>' +
686 '</ul>' +
739 '</ul>' +
687 '</li>' +
740 '</li>' +
688 '</ul>'
741 '</ul>'
689
742
690 @project = Project.find(1)
743 @project = Project.find(1)
691 assert textilizable(raw).gsub("\n", "").include?(expected), textilizable(raw)
744 assert textilizable(raw).gsub("\n", "").include?(expected), textilizable(raw)
692 end
745 end
693
746
694 def test_table_of_content_should_contain_included_page_headings
747 def test_table_of_content_should_contain_included_page_headings
695 raw = <<-RAW
748 raw = <<-RAW
696 {{toc}}
749 {{toc}}
697
750
698 h1. Included
751 h1. Included
699
752
700 {{include(Child_1)}}
753 {{include(Child_1)}}
701 RAW
754 RAW
702
755
703 expected = '<ul class="toc">' +
756 expected = '<ul class="toc">' +
704 '<li><a href="#Included">Included</a></li>' +
757 '<li><a href="#Included">Included</a></li>' +
705 '<li><a href="#Child-page-1">Child page 1</a></li>' +
758 '<li><a href="#Child-page-1">Child page 1</a></li>' +
706 '</ul>'
759 '</ul>'
707
760
708 @project = Project.find(1)
761 @project = Project.find(1)
709 assert textilizable(raw).gsub("\n", "").include?(expected)
762 assert textilizable(raw).gsub("\n", "").include?(expected)
710 end
763 end
711
764
712 def test_default_formatter
765 def test_default_formatter
713 Setting.text_formatting = 'unknown'
766 Setting.text_formatting = 'unknown'
714 text = 'a *link*: http://www.example.net/'
767 text = 'a *link*: http://www.example.net/'
715 assert_equal '<p>a *link*: <a href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
768 assert_equal '<p>a *link*: <a href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
716 Setting.text_formatting = 'textile'
769 Setting.text_formatting = 'textile'
717 end
770 end
718
771
719 def test_due_date_distance_in_words
772 def test_due_date_distance_in_words
720 to_test = { Date.today => 'Due in 0 days',
773 to_test = { Date.today => 'Due in 0 days',
721 Date.today + 1 => 'Due in 1 day',
774 Date.today + 1 => 'Due in 1 day',
722 Date.today + 100 => 'Due in about 3 months',
775 Date.today + 100 => 'Due in about 3 months',
723 Date.today + 20000 => 'Due in over 54 years',
776 Date.today + 20000 => 'Due in over 54 years',
724 Date.today - 1 => '1 day late',
777 Date.today - 1 => '1 day late',
725 Date.today - 100 => 'about 3 months late',
778 Date.today - 100 => 'about 3 months late',
726 Date.today - 20000 => 'over 54 years late',
779 Date.today - 20000 => 'over 54 years late',
727 }
780 }
728 ::I18n.locale = :en
781 ::I18n.locale = :en
729 to_test.each do |date, expected|
782 to_test.each do |date, expected|
730 assert_equal expected, due_date_distance_in_words(date)
783 assert_equal expected, due_date_distance_in_words(date)
731 end
784 end
732 end
785 end
733
786
734 def test_avatar
787 def test_avatar
735 # turn on avatars
788 # turn on avatars
736 Setting.gravatar_enabled = '1'
789 Setting.gravatar_enabled = '1'
737 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
790 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
738 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
791 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
739 assert_nil avatar('jsmith')
792 assert_nil avatar('jsmith')
740 assert_nil avatar(nil)
793 assert_nil avatar(nil)
741
794
742 # turn off avatars
795 # turn off avatars
743 Setting.gravatar_enabled = '0'
796 Setting.gravatar_enabled = '0'
744 assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
797 assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
745 end
798 end
746
799
747 def test_link_to_user
800 def test_link_to_user
748 user = User.find(2)
801 user = User.find(2)
749 t = link_to_user(user)
802 t = link_to_user(user)
750 assert_equal "<a href=\"/users/2\">#{ user.name }</a>", t
803 assert_equal "<a href=\"/users/2\">#{ user.name }</a>", t
751 end
804 end
752
805
753 def test_link_to_user_should_not_link_to_locked_user
806 def test_link_to_user_should_not_link_to_locked_user
754 user = User.find(5)
807 user = User.find(5)
755 assert user.locked?
808 assert user.locked?
756 t = link_to_user(user)
809 t = link_to_user(user)
757 assert_equal user.name, t
810 assert_equal user.name, t
758 end
811 end
759
812
760 def test_link_to_user_should_not_link_to_anonymous
813 def test_link_to_user_should_not_link_to_anonymous
761 user = User.anonymous
814 user = User.anonymous
762 assert user.anonymous?
815 assert user.anonymous?
763 t = link_to_user(user)
816 t = link_to_user(user)
764 assert_equal ::I18n.t(:label_user_anonymous), t
817 assert_equal ::I18n.t(:label_user_anonymous), t
765 end
818 end
766
819
767 def test_link_to_project
820 def test_link_to_project
768 project = Project.find(1)
821 project = Project.find(1)
769 assert_equal %(<a href="/projects/ecookbook">eCookbook</a>),
822 assert_equal %(<a href="/projects/ecookbook">eCookbook</a>),
770 link_to_project(project)
823 link_to_project(project)
771 assert_equal %(<a href="/projects/ecookbook/settings">eCookbook</a>),
824 assert_equal %(<a href="/projects/ecookbook/settings">eCookbook</a>),
772 link_to_project(project, :action => 'settings')
825 link_to_project(project, :action => 'settings')
773 assert_equal %(<a href="http://test.host/projects/ecookbook?jump=blah">eCookbook</a>),
826 assert_equal %(<a href="http://test.host/projects/ecookbook?jump=blah">eCookbook</a>),
774 link_to_project(project, {:only_path => false, :jump => 'blah'})
827 link_to_project(project, {:only_path => false, :jump => 'blah'})
775 assert_equal %(<a href="/projects/ecookbook/settings" class="project">eCookbook</a>),
828 assert_equal %(<a href="/projects/ecookbook/settings" class="project">eCookbook</a>),
776 link_to_project(project, {:action => 'settings'}, :class => "project")
829 link_to_project(project, {:action => 'settings'}, :class => "project")
777 end
830 end
778
831
779 def test_principals_options_for_select_with_users
832 def test_principals_options_for_select_with_users
780 users = [User.find(2), User.find(4)]
833 users = [User.find(2), User.find(4)]
781 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>),
834 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>),
782 principals_options_for_select(users)
835 principals_options_for_select(users)
783 end
836 end
784
837
785 def test_principals_options_for_select_with_selected
838 def test_principals_options_for_select_with_selected
786 users = [User.find(2), User.find(4)]
839 users = [User.find(2), User.find(4)]
787 assert_equal %(<option value="2">John Smith</option><option value="4" selected="selected">Robert Hill</option>),
840 assert_equal %(<option value="2">John Smith</option><option value="4" selected="selected">Robert Hill</option>),
788 principals_options_for_select(users, User.find(4))
841 principals_options_for_select(users, User.find(4))
789 end
842 end
790
843
791 def test_principals_options_for_select_with_users_and_groups
844 def test_principals_options_for_select_with_users_and_groups
792 users = [User.find(2), Group.find(11), User.find(4), Group.find(10)]
845 users = [User.find(2), Group.find(11), User.find(4), Group.find(10)]
793 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>) +
846 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>) +
794 %(<optgroup label="Groups"><option value="10">A Team</option><option value="11">B Team</option></optgroup>),
847 %(<optgroup label="Groups"><option value="10">A Team</option><option value="11">B Team</option></optgroup>),
795 principals_options_for_select(users)
848 principals_options_for_select(users)
796 end
849 end
797
850
798 def test_principals_options_for_select_with_empty_collection
851 def test_principals_options_for_select_with_empty_collection
799 assert_equal '', principals_options_for_select([])
852 assert_equal '', principals_options_for_select([])
800 end
853 end
801 end
854 end
General Comments 0
You need to be logged in to leave comments. Login now