##// END OF EJS Templates
Merged r10580 and r10581 from trunk....
Jean-Philippe Lang -
r10510:d02e5e54a825
parent child
Show More
@@ -1,1274 +1,1273
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2012 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 # Displays a link to user's account page if active
46 # Displays a link to user's account page if active
47 def link_to_user(user, options={})
47 def link_to_user(user, options={})
48 if user.is_a?(User)
48 if user.is_a?(User)
49 name = h(user.name(options[:format]))
49 name = h(user.name(options[:format]))
50 if user.active?
50 if user.active?
51 link_to name, :controller => 'users', :action => 'show', :id => user
51 link_to name, :controller => 'users', :action => 'show', :id => user
52 else
52 else
53 name
53 name
54 end
54 end
55 else
55 else
56 h(user.to_s)
56 h(user.to_s)
57 end
57 end
58 end
58 end
59
59
60 # Displays a link to +issue+ with its subject.
60 # Displays a link to +issue+ with its subject.
61 # Examples:
61 # Examples:
62 #
62 #
63 # link_to_issue(issue) # => Defect #6: This is the subject
63 # link_to_issue(issue) # => Defect #6: This is the subject
64 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
64 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
65 # link_to_issue(issue, :subject => false) # => Defect #6
65 # link_to_issue(issue, :subject => false) # => Defect #6
66 # link_to_issue(issue, :project => true) # => Foo - Defect #6
66 # link_to_issue(issue, :project => true) # => Foo - Defect #6
67 #
67 #
68 def link_to_issue(issue, options={})
68 def link_to_issue(issue, options={})
69 title = nil
69 title = nil
70 subject = nil
70 subject = nil
71 if options[:subject] == false
71 if options[:subject] == false
72 title = truncate(issue.subject, :length => 60)
72 title = truncate(issue.subject, :length => 60)
73 else
73 else
74 subject = issue.subject
74 subject = issue.subject
75 if options[:truncate]
75 if options[:truncate]
76 subject = truncate(subject, :length => options[:truncate])
76 subject = truncate(subject, :length => options[:truncate])
77 end
77 end
78 end
78 end
79 s = link_to "#{h(issue.tracker)} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
79 s = link_to "#{h(issue.tracker)} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
80 :class => issue.css_classes,
80 :class => issue.css_classes,
81 :title => title
81 :title => title
82 s << h(": #{subject}") if subject
82 s << h(": #{subject}") if subject
83 s = h("#{issue.project} - ") + s if options[:project]
83 s = h("#{issue.project} - ") + s if options[:project]
84 s
84 s
85 end
85 end
86
86
87 # Generates a link to an attachment.
87 # Generates a link to an attachment.
88 # Options:
88 # Options:
89 # * :text - Link text (default to attachment filename)
89 # * :text - Link text (default to attachment filename)
90 # * :download - Force download (default: false)
90 # * :download - Force download (default: false)
91 def link_to_attachment(attachment, options={})
91 def link_to_attachment(attachment, options={})
92 text = options.delete(:text) || attachment.filename
92 text = options.delete(:text) || attachment.filename
93 action = options.delete(:download) ? 'download' : 'show'
93 action = options.delete(:download) ? 'download' : 'show'
94 opt_only_path = {}
94 opt_only_path = {}
95 opt_only_path[:only_path] = (options[:only_path] == false ? false : true)
95 opt_only_path[:only_path] = (options[:only_path] == false ? false : true)
96 options.delete(:only_path)
96 options.delete(:only_path)
97 link_to(h(text),
97 link_to(h(text),
98 {:controller => 'attachments', :action => action,
98 {:controller => 'attachments', :action => action,
99 :id => attachment, :filename => attachment.filename}.merge(opt_only_path),
99 :id => attachment, :filename => attachment.filename}.merge(opt_only_path),
100 options)
100 options)
101 end
101 end
102
102
103 # Generates a link to a SCM revision
103 # Generates a link to a SCM revision
104 # Options:
104 # Options:
105 # * :text - Link text (default to the formatted revision)
105 # * :text - Link text (default to the formatted revision)
106 def link_to_revision(revision, repository, options={})
106 def link_to_revision(revision, repository, options={})
107 if repository.is_a?(Project)
107 if repository.is_a?(Project)
108 repository = repository.repository
108 repository = repository.repository
109 end
109 end
110 text = options.delete(:text) || format_revision(revision)
110 text = options.delete(:text) || format_revision(revision)
111 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
111 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
112 link_to(
112 link_to(
113 h(text),
113 h(text),
114 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
114 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
115 :title => l(:label_revision_id, format_revision(revision))
115 :title => l(:label_revision_id, format_revision(revision))
116 )
116 )
117 end
117 end
118
118
119 # Generates a link to a message
119 # Generates a link to a message
120 def link_to_message(message, options={}, html_options = nil)
120 def link_to_message(message, options={}, html_options = nil)
121 link_to(
121 link_to(
122 h(truncate(message.subject, :length => 60)),
122 h(truncate(message.subject, :length => 60)),
123 { :controller => 'messages', :action => 'show',
123 { :controller => 'messages', :action => 'show',
124 :board_id => message.board_id,
124 :board_id => message.board_id,
125 :id => (message.parent_id || message.id),
125 :id => (message.parent_id || message.id),
126 :r => (message.parent_id && message.id),
126 :r => (message.parent_id && message.id),
127 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
127 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
128 }.merge(options),
128 }.merge(options),
129 html_options
129 html_options
130 )
130 )
131 end
131 end
132
132
133 # Generates a link to a project if active
133 # Generates a link to a project if active
134 # Examples:
134 # Examples:
135 #
135 #
136 # link_to_project(project) # => link to the specified project overview
136 # link_to_project(project) # => link to the specified project overview
137 # link_to_project(project, :action=>'settings') # => link to project settings
137 # link_to_project(project, :action=>'settings') # => link to project settings
138 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
138 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
139 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
139 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
140 #
140 #
141 def link_to_project(project, options={}, html_options = nil)
141 def link_to_project(project, options={}, html_options = nil)
142 if project.archived?
142 if project.archived?
143 h(project)
143 h(project)
144 else
144 else
145 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
145 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
146 link_to(h(project), url, html_options)
146 link_to(h(project), url, html_options)
147 end
147 end
148 end
148 end
149
149
150 def thumbnail_tag(attachment)
150 def thumbnail_tag(attachment)
151 link_to image_tag(url_for(:controller => 'attachments', :action => 'thumbnail', :id => attachment)),
151 link_to image_tag(url_for(:controller => 'attachments', :action => 'thumbnail', :id => attachment)),
152 {:controller => 'attachments', :action => 'show', :id => attachment, :filename => attachment.filename},
152 {:controller => 'attachments', :action => 'show', :id => attachment, :filename => attachment.filename},
153 :title => attachment.filename
153 :title => attachment.filename
154 end
154 end
155
155
156 def toggle_link(name, id, options={})
156 def toggle_link(name, id, options={})
157 onclick = "$('##{id}').toggle(); "
157 onclick = "$('##{id}').toggle(); "
158 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
158 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
159 onclick << "return false;"
159 onclick << "return false;"
160 link_to(name, "#", :onclick => onclick)
160 link_to(name, "#", :onclick => onclick)
161 end
161 end
162
162
163 def image_to_function(name, function, html_options = {})
163 def image_to_function(name, function, html_options = {})
164 html_options.symbolize_keys!
164 html_options.symbolize_keys!
165 tag(:input, html_options.merge({
165 tag(:input, html_options.merge({
166 :type => "image", :src => image_path(name),
166 :type => "image", :src => image_path(name),
167 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
167 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
168 }))
168 }))
169 end
169 end
170
170
171 def format_activity_title(text)
171 def format_activity_title(text)
172 h(truncate_single_line(text, :length => 100))
172 h(truncate_single_line(text, :length => 100))
173 end
173 end
174
174
175 def format_activity_day(date)
175 def format_activity_day(date)
176 date == User.current.today ? l(:label_today).titleize : format_date(date)
176 date == User.current.today ? l(:label_today).titleize : format_date(date)
177 end
177 end
178
178
179 def format_activity_description(text)
179 def format_activity_description(text)
180 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
180 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
181 ).gsub(/[\r\n]+/, "<br />").html_safe
181 ).gsub(/[\r\n]+/, "<br />").html_safe
182 end
182 end
183
183
184 def format_version_name(version)
184 def format_version_name(version)
185 if version.project == @project
185 if version.project == @project
186 h(version)
186 h(version)
187 else
187 else
188 h("#{version.project} - #{version}")
188 h("#{version.project} - #{version}")
189 end
189 end
190 end
190 end
191
191
192 def due_date_distance_in_words(date)
192 def due_date_distance_in_words(date)
193 if date
193 if date
194 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
194 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
195 end
195 end
196 end
196 end
197
197
198 # Renders a tree of projects as a nested set of unordered lists
198 # Renders a tree of projects as a nested set of unordered lists
199 # The given collection may be a subset of the whole project tree
199 # The given collection may be a subset of the whole project tree
200 # (eg. some intermediate nodes are private and can not be seen)
200 # (eg. some intermediate nodes are private and can not be seen)
201 def render_project_nested_lists(projects)
201 def render_project_nested_lists(projects)
202 s = ''
202 s = ''
203 if projects.any?
203 if projects.any?
204 ancestors = []
204 ancestors = []
205 original_project = @project
205 original_project = @project
206 projects.sort_by(&:lft).each do |project|
206 projects.sort_by(&:lft).each do |project|
207 # set the project environment to please macros.
207 # set the project environment to please macros.
208 @project = project
208 @project = project
209 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
209 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
210 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
210 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
211 else
211 else
212 ancestors.pop
212 ancestors.pop
213 s << "</li>"
213 s << "</li>"
214 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
214 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
215 ancestors.pop
215 ancestors.pop
216 s << "</ul></li>\n"
216 s << "</ul></li>\n"
217 end
217 end
218 end
218 end
219 classes = (ancestors.empty? ? 'root' : 'child')
219 classes = (ancestors.empty? ? 'root' : 'child')
220 s << "<li class='#{classes}'><div class='#{classes}'>"
220 s << "<li class='#{classes}'><div class='#{classes}'>"
221 s << h(block_given? ? yield(project) : project.name)
221 s << h(block_given? ? yield(project) : project.name)
222 s << "</div>\n"
222 s << "</div>\n"
223 ancestors << project
223 ancestors << project
224 end
224 end
225 s << ("</li></ul>\n" * ancestors.size)
225 s << ("</li></ul>\n" * ancestors.size)
226 @project = original_project
226 @project = original_project
227 end
227 end
228 s.html_safe
228 s.html_safe
229 end
229 end
230
230
231 def render_page_hierarchy(pages, node=nil, options={})
231 def render_page_hierarchy(pages, node=nil, options={})
232 content = ''
232 content = ''
233 if pages[node]
233 if pages[node]
234 content << "<ul class=\"pages-hierarchy\">\n"
234 content << "<ul class=\"pages-hierarchy\">\n"
235 pages[node].each do |page|
235 pages[node].each do |page|
236 content << "<li>"
236 content << "<li>"
237 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
237 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
238 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
238 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
239 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
239 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
240 content << "</li>\n"
240 content << "</li>\n"
241 end
241 end
242 content << "</ul>\n"
242 content << "</ul>\n"
243 end
243 end
244 content.html_safe
244 content.html_safe
245 end
245 end
246
246
247 # Renders flash messages
247 # Renders flash messages
248 def render_flash_messages
248 def render_flash_messages
249 s = ''
249 s = ''
250 flash.each do |k,v|
250 flash.each do |k,v|
251 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
251 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
252 end
252 end
253 s.html_safe
253 s.html_safe
254 end
254 end
255
255
256 # Renders tabs and their content
256 # Renders tabs and their content
257 def render_tabs(tabs)
257 def render_tabs(tabs)
258 if tabs.any?
258 if tabs.any?
259 render :partial => 'common/tabs', :locals => {:tabs => tabs}
259 render :partial => 'common/tabs', :locals => {:tabs => tabs}
260 else
260 else
261 content_tag 'p', l(:label_no_data), :class => "nodata"
261 content_tag 'p', l(:label_no_data), :class => "nodata"
262 end
262 end
263 end
263 end
264
264
265 # Renders the project quick-jump box
265 # Renders the project quick-jump box
266 def render_project_jump_box
266 def render_project_jump_box
267 return unless User.current.logged?
267 return unless User.current.logged?
268 projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
268 projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
269 if projects.any?
269 if projects.any?
270 options =
270 options =
271 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
271 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
272 '<option value="" disabled="disabled">---</option>').html_safe
272 '<option value="" disabled="disabled">---</option>').html_safe
273
273
274 options << project_tree_options_for_select(projects, :selected => @project) do |p|
274 options << project_tree_options_for_select(projects, :selected => @project) do |p|
275 { :value => project_path(:id => p, :jump => current_menu_item) }
275 { :value => project_path(:id => p, :jump => current_menu_item) }
276 end
276 end
277
277
278 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
278 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
279 end
279 end
280 end
280 end
281
281
282 def project_tree_options_for_select(projects, options = {})
282 def project_tree_options_for_select(projects, options = {})
283 s = ''
283 s = ''
284 project_tree(projects) do |project, level|
284 project_tree(projects) do |project, level|
285 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
285 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
286 tag_options = {:value => project.id}
286 tag_options = {:value => project.id}
287 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
287 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
288 tag_options[:selected] = 'selected'
288 tag_options[:selected] = 'selected'
289 else
289 else
290 tag_options[:selected] = nil
290 tag_options[:selected] = nil
291 end
291 end
292 tag_options.merge!(yield(project)) if block_given?
292 tag_options.merge!(yield(project)) if block_given?
293 s << content_tag('option', name_prefix + h(project), tag_options)
293 s << content_tag('option', name_prefix + h(project), tag_options)
294 end
294 end
295 s.html_safe
295 s.html_safe
296 end
296 end
297
297
298 # Yields the given block for each project with its level in the tree
298 # Yields the given block for each project with its level in the tree
299 #
299 #
300 # Wrapper for Project#project_tree
300 # Wrapper for Project#project_tree
301 def project_tree(projects, &block)
301 def project_tree(projects, &block)
302 Project.project_tree(projects, &block)
302 Project.project_tree(projects, &block)
303 end
303 end
304
304
305 def principals_check_box_tags(name, principals)
305 def principals_check_box_tags(name, principals)
306 s = ''
306 s = ''
307 principals.sort.each do |principal|
307 principals.sort.each do |principal|
308 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
308 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
309 end
309 end
310 s.html_safe
310 s.html_safe
311 end
311 end
312
312
313 # Returns a string for users/groups option tags
313 # Returns a string for users/groups option tags
314 def principals_options_for_select(collection, selected=nil)
314 def principals_options_for_select(collection, selected=nil)
315 s = ''
315 s = ''
316 if collection.include?(User.current)
316 if collection.include?(User.current)
317 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
317 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
318 end
318 end
319 groups = ''
319 groups = ''
320 collection.sort.each do |element|
320 collection.sort.each do |element|
321 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected)
321 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected)
322 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
322 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
323 end
323 end
324 unless groups.empty?
324 unless groups.empty?
325 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
325 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
326 end
326 end
327 s.html_safe
327 s.html_safe
328 end
328 end
329
329
330 # Truncates and returns the string as a single line
330 # Truncates and returns the string as a single line
331 def truncate_single_line(string, *args)
331 def truncate_single_line(string, *args)
332 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
332 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
333 end
333 end
334
334
335 # Truncates at line break after 250 characters or options[:length]
335 # Truncates at line break after 250 characters or options[:length]
336 def truncate_lines(string, options={})
336 def truncate_lines(string, options={})
337 length = options[:length] || 250
337 length = options[:length] || 250
338 if string.to_s =~ /\A(.{#{length}}.*?)$/m
338 if string.to_s =~ /\A(.{#{length}}.*?)$/m
339 "#{$1}..."
339 "#{$1}..."
340 else
340 else
341 string
341 string
342 end
342 end
343 end
343 end
344
344
345 def anchor(text)
345 def anchor(text)
346 text.to_s.gsub(' ', '_')
346 text.to_s.gsub(' ', '_')
347 end
347 end
348
348
349 def html_hours(text)
349 def html_hours(text)
350 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
350 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
351 end
351 end
352
352
353 def authoring(created, author, options={})
353 def authoring(created, author, options={})
354 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
354 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
355 end
355 end
356
356
357 def time_tag(time)
357 def time_tag(time)
358 text = distance_of_time_in_words(Time.now, time)
358 text = distance_of_time_in_words(Time.now, time)
359 if @project
359 if @project
360 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
360 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
361 else
361 else
362 content_tag('acronym', text, :title => format_time(time))
362 content_tag('acronym', text, :title => format_time(time))
363 end
363 end
364 end
364 end
365
365
366 def syntax_highlight_lines(name, content)
366 def syntax_highlight_lines(name, content)
367 lines = []
367 lines = []
368 syntax_highlight(name, content).each_line { |line| lines << line }
368 syntax_highlight(name, content).each_line { |line| lines << line }
369 lines
369 lines
370 end
370 end
371
371
372 def syntax_highlight(name, content)
372 def syntax_highlight(name, content)
373 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
373 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
374 end
374 end
375
375
376 def to_path_param(path)
376 def to_path_param(path)
377 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
377 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
378 str.blank? ? nil : str
378 str.blank? ? nil : str
379 end
379 end
380
380
381 def pagination_links_full(paginator, count=nil, options={})
381 def pagination_links_full(paginator, count=nil, options={})
382 page_param = options.delete(:page_param) || :page
382 page_param = options.delete(:page_param) || :page
383 per_page_links = options.delete(:per_page_links)
383 per_page_links = options.delete(:per_page_links)
384 url_param = params.dup
384 url_param = params.dup
385
385
386 html = ''
386 html = ''
387 if paginator.current.previous
387 if paginator.current.previous
388 # \xc2\xab(utf-8) = &#171;
388 # \xc2\xab(utf-8) = &#171;
389 html << link_to_content_update(
389 html << link_to_content_update(
390 "\xc2\xab " + l(:label_previous),
390 "\xc2\xab " + l(:label_previous),
391 url_param.merge(page_param => paginator.current.previous)) + ' '
391 url_param.merge(page_param => paginator.current.previous)) + ' '
392 end
392 end
393
393
394 html << (pagination_links_each(paginator, options) do |n|
394 html << (pagination_links_each(paginator, options) do |n|
395 link_to_content_update(n.to_s, url_param.merge(page_param => n))
395 link_to_content_update(n.to_s, url_param.merge(page_param => n))
396 end || '')
396 end || '')
397
397
398 if paginator.current.next
398 if paginator.current.next
399 # \xc2\xbb(utf-8) = &#187;
399 # \xc2\xbb(utf-8) = &#187;
400 html << ' ' + link_to_content_update(
400 html << ' ' + link_to_content_update(
401 (l(:label_next) + " \xc2\xbb"),
401 (l(:label_next) + " \xc2\xbb"),
402 url_param.merge(page_param => paginator.current.next))
402 url_param.merge(page_param => paginator.current.next))
403 end
403 end
404
404
405 unless count.nil?
405 unless count.nil?
406 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
406 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
407 if per_page_links != false && links = per_page_links(paginator.items_per_page, count)
407 if per_page_links != false && links = per_page_links(paginator.items_per_page, count)
408 html << " | #{links}"
408 html << " | #{links}"
409 end
409 end
410 end
410 end
411
411
412 html.html_safe
412 html.html_safe
413 end
413 end
414
414
415 def per_page_links(selected=nil, item_count=nil)
415 def per_page_links(selected=nil, item_count=nil)
416 values = Setting.per_page_options_array
416 values = Setting.per_page_options_array
417 if item_count && values.any?
417 if item_count && values.any?
418 if item_count > values.first
418 if item_count > values.first
419 max = values.detect {|value| value >= item_count} || item_count
419 max = values.detect {|value| value >= item_count} || item_count
420 else
420 else
421 max = item_count
421 max = item_count
422 end
422 end
423 values = values.select {|value| value <= max || value == selected}
423 values = values.select {|value| value <= max || value == selected}
424 end
424 end
425 if values.empty? || (values.size == 1 && values.first == selected)
425 if values.empty? || (values.size == 1 && values.first == selected)
426 return nil
426 return nil
427 end
427 end
428 links = values.collect do |n|
428 links = values.collect do |n|
429 n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
429 n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
430 end
430 end
431 l(:label_display_per_page, links.join(', '))
431 l(:label_display_per_page, links.join(', '))
432 end
432 end
433
433
434 def reorder_links(name, url, method = :post)
434 def reorder_links(name, url, method = :post)
435 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
435 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
436 url.merge({"#{name}[move_to]" => 'highest'}),
436 url.merge({"#{name}[move_to]" => 'highest'}),
437 :method => method, :title => l(:label_sort_highest)) +
437 :method => method, :title => l(:label_sort_highest)) +
438 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
438 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
439 url.merge({"#{name}[move_to]" => 'higher'}),
439 url.merge({"#{name}[move_to]" => 'higher'}),
440 :method => method, :title => l(:label_sort_higher)) +
440 :method => method, :title => l(:label_sort_higher)) +
441 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
441 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
442 url.merge({"#{name}[move_to]" => 'lower'}),
442 url.merge({"#{name}[move_to]" => 'lower'}),
443 :method => method, :title => l(:label_sort_lower)) +
443 :method => method, :title => l(:label_sort_lower)) +
444 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
444 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
445 url.merge({"#{name}[move_to]" => 'lowest'}),
445 url.merge({"#{name}[move_to]" => 'lowest'}),
446 :method => method, :title => l(:label_sort_lowest))
446 :method => method, :title => l(:label_sort_lowest))
447 end
447 end
448
448
449 def breadcrumb(*args)
449 def breadcrumb(*args)
450 elements = args.flatten
450 elements = args.flatten
451 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
451 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
452 end
452 end
453
453
454 def other_formats_links(&block)
454 def other_formats_links(&block)
455 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
455 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
456 yield Redmine::Views::OtherFormatsBuilder.new(self)
456 yield Redmine::Views::OtherFormatsBuilder.new(self)
457 concat('</p>'.html_safe)
457 concat('</p>'.html_safe)
458 end
458 end
459
459
460 def page_header_title
460 def page_header_title
461 if @project.nil? || @project.new_record?
461 if @project.nil? || @project.new_record?
462 h(Setting.app_title)
462 h(Setting.app_title)
463 else
463 else
464 b = []
464 b = []
465 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
465 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
466 if ancestors.any?
466 if ancestors.any?
467 root = ancestors.shift
467 root = ancestors.shift
468 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
468 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
469 if ancestors.size > 2
469 if ancestors.size > 2
470 b << "\xe2\x80\xa6"
470 b << "\xe2\x80\xa6"
471 ancestors = ancestors[-2, 2]
471 ancestors = ancestors[-2, 2]
472 end
472 end
473 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
473 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
474 end
474 end
475 b << h(@project)
475 b << h(@project)
476 b.join(" \xc2\xbb ").html_safe
476 b.join(" \xc2\xbb ").html_safe
477 end
477 end
478 end
478 end
479
479
480 def html_title(*args)
480 def html_title(*args)
481 if args.empty?
481 if args.empty?
482 title = @html_title || []
482 title = @html_title || []
483 title << @project.name if @project
483 title << @project.name if @project
484 title << Setting.app_title unless Setting.app_title == title.last
484 title << Setting.app_title unless Setting.app_title == title.last
485 title.select {|t| !t.blank? }.join(' - ')
485 title.select {|t| !t.blank? }.join(' - ')
486 else
486 else
487 @html_title ||= []
487 @html_title ||= []
488 @html_title += args
488 @html_title += args
489 end
489 end
490 end
490 end
491
491
492 # Returns the theme, controller name, and action as css classes for the
492 # Returns the theme, controller name, and action as css classes for the
493 # HTML body.
493 # HTML body.
494 def body_css_classes
494 def body_css_classes
495 css = []
495 css = []
496 if theme = Redmine::Themes.theme(Setting.ui_theme)
496 if theme = Redmine::Themes.theme(Setting.ui_theme)
497 css << 'theme-' + theme.name
497 css << 'theme-' + theme.name
498 end
498 end
499
499
500 css << 'controller-' + controller_name
500 css << 'controller-' + controller_name
501 css << 'action-' + action_name
501 css << 'action-' + action_name
502 css.join(' ')
502 css.join(' ')
503 end
503 end
504
504
505 def accesskey(s)
505 def accesskey(s)
506 Redmine::AccessKeys.key_for s
506 Redmine::AccessKeys.key_for s
507 end
507 end
508
508
509 # Formats text according to system settings.
509 # Formats text according to system settings.
510 # 2 ways to call this method:
510 # 2 ways to call this method:
511 # * with a String: textilizable(text, options)
511 # * with a String: textilizable(text, options)
512 # * with an object and one of its attribute: textilizable(issue, :description, options)
512 # * with an object and one of its attribute: textilizable(issue, :description, options)
513 def textilizable(*args)
513 def textilizable(*args)
514 options = args.last.is_a?(Hash) ? args.pop : {}
514 options = args.last.is_a?(Hash) ? args.pop : {}
515 case args.size
515 case args.size
516 when 1
516 when 1
517 obj = options[:object]
517 obj = options[:object]
518 text = args.shift
518 text = args.shift
519 when 2
519 when 2
520 obj = args.shift
520 obj = args.shift
521 attr = args.shift
521 attr = args.shift
522 text = obj.send(attr).to_s
522 text = obj.send(attr).to_s
523 else
523 else
524 raise ArgumentError, 'invalid arguments to textilizable'
524 raise ArgumentError, 'invalid arguments to textilizable'
525 end
525 end
526 return '' if text.blank?
526 return '' if text.blank?
527 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
527 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
528 only_path = options.delete(:only_path) == false ? false : true
528 only_path = options.delete(:only_path) == false ? false : true
529
529
530 text = text.dup
530 text = text.dup
531 macros = catch_macros(text)
531 macros = catch_macros(text)
532 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
532 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
533
533
534 @parsed_headings = []
534 @parsed_headings = []
535 @heading_anchors = {}
535 @heading_anchors = {}
536 @current_section = 0 if options[:edit_section_links]
536 @current_section = 0 if options[:edit_section_links]
537
537
538 parse_sections(text, project, obj, attr, only_path, options)
538 parse_sections(text, project, obj, attr, only_path, options)
539 text = parse_non_pre_blocks(text, obj, macros) do |text|
539 text = parse_non_pre_blocks(text, obj, macros) do |text|
540 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
540 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
541 send method_name, text, project, obj, attr, only_path, options
541 send method_name, text, project, obj, attr, only_path, options
542 end
542 end
543 end
543 end
544 parse_headings(text, project, obj, attr, only_path, options)
544 parse_headings(text, project, obj, attr, only_path, options)
545
545
546 if @parsed_headings.any?
546 if @parsed_headings.any?
547 replace_toc(text, @parsed_headings)
547 replace_toc(text, @parsed_headings)
548 end
548 end
549
549
550 text.html_safe
550 text.html_safe
551 end
551 end
552
552
553 def parse_non_pre_blocks(text, obj, macros)
553 def parse_non_pre_blocks(text, obj, macros)
554 s = StringScanner.new(text)
554 s = StringScanner.new(text)
555 tags = []
555 tags = []
556 parsed = ''
556 parsed = ''
557 while !s.eos?
557 while !s.eos?
558 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
558 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
559 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
559 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
560 if tags.empty?
560 if tags.empty?
561 yield text
561 yield text
562 inject_macros(text, obj, macros) if macros.any?
562 inject_macros(text, obj, macros) if macros.any?
563 else
563 else
564 inject_macros(text, obj, macros, false) if macros.any?
564 inject_macros(text, obj, macros, false) if macros.any?
565 end
565 end
566 parsed << text
566 parsed << text
567 if tag
567 if tag
568 if closing
568 if closing
569 if tags.last == tag.downcase
569 if tags.last == tag.downcase
570 tags.pop
570 tags.pop
571 end
571 end
572 else
572 else
573 tags << tag.downcase
573 tags << tag.downcase
574 end
574 end
575 parsed << full_tag
575 parsed << full_tag
576 end
576 end
577 end
577 end
578 # Close any non closing tags
578 # Close any non closing tags
579 while tag = tags.pop
579 while tag = tags.pop
580 parsed << "</#{tag}>"
580 parsed << "</#{tag}>"
581 end
581 end
582 parsed
582 parsed
583 end
583 end
584
584
585 def parse_inline_attachments(text, project, obj, attr, only_path, options)
585 def parse_inline_attachments(text, project, obj, attr, only_path, options)
586 # when using an image link, try to use an attachment, if possible
586 # when using an image link, try to use an attachment, if possible
587 if options[:attachments] || (obj && obj.respond_to?(:attachments))
587 if options[:attachments] || (obj && obj.respond_to?(:attachments))
588 attachments = options[:attachments] || obj.attachments
588 attachments = options[:attachments] || obj.attachments
589 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
589 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
590 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
590 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
591 # search for the picture in attachments
591 # search for the picture in attachments
592 if found = Attachment.latest_attach(attachments, filename)
592 if found = Attachment.latest_attach(attachments, filename)
593 image_url = url_for :only_path => only_path, :controller => 'attachments',
593 image_url = url_for :only_path => only_path, :controller => 'attachments',
594 :action => 'download', :id => found
594 :action => 'download', :id => found
595 desc = found.description.to_s.gsub('"', '')
595 desc = found.description.to_s.gsub('"', '')
596 if !desc.blank? && alttext.blank?
596 if !desc.blank? && alttext.blank?
597 alt = " title=\"#{desc}\" alt=\"#{desc}\""
597 alt = " title=\"#{desc}\" alt=\"#{desc}\""
598 end
598 end
599 "src=\"#{image_url}\"#{alt}"
599 "src=\"#{image_url}\"#{alt}"
600 else
600 else
601 m
601 m
602 end
602 end
603 end
603 end
604 end
604 end
605 end
605 end
606
606
607 # Wiki links
607 # Wiki links
608 #
608 #
609 # Examples:
609 # Examples:
610 # [[mypage]]
610 # [[mypage]]
611 # [[mypage|mytext]]
611 # [[mypage|mytext]]
612 # wiki links can refer other project wikis, using project name or identifier:
612 # wiki links can refer other project wikis, using project name or identifier:
613 # [[project:]] -> wiki starting page
613 # [[project:]] -> wiki starting page
614 # [[project:|mytext]]
614 # [[project:|mytext]]
615 # [[project:mypage]]
615 # [[project:mypage]]
616 # [[project:mypage|mytext]]
616 # [[project:mypage|mytext]]
617 def parse_wiki_links(text, project, obj, attr, only_path, options)
617 def parse_wiki_links(text, project, obj, attr, only_path, options)
618 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
618 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
619 link_project = project
619 link_project = project
620 esc, all, page, title = $1, $2, $3, $5
620 esc, all, page, title = $1, $2, $3, $5
621 if esc.nil?
621 if esc.nil?
622 if page =~ /^([^\:]+)\:(.*)$/
622 if page =~ /^([^\:]+)\:(.*)$/
623 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
623 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
624 page = $2
624 page = $2
625 title ||= $1 if page.blank?
625 title ||= $1 if page.blank?
626 end
626 end
627
627
628 if link_project && link_project.wiki
628 if link_project && link_project.wiki
629 # extract anchor
629 # extract anchor
630 anchor = nil
630 anchor = nil
631 if page =~ /^(.+?)\#(.+)$/
631 if page =~ /^(.+?)\#(.+)$/
632 page, anchor = $1, $2
632 page, anchor = $1, $2
633 end
633 end
634 anchor = sanitize_anchor_name(anchor) if anchor.present?
634 anchor = sanitize_anchor_name(anchor) if anchor.present?
635 # check if page exists
635 # check if page exists
636 wiki_page = link_project.wiki.find_page(page)
636 wiki_page = link_project.wiki.find_page(page)
637 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
637 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
638 "##{anchor}"
638 "##{anchor}"
639 else
639 else
640 case options[:wiki_links]
640 case options[:wiki_links]
641 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
641 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
642 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
642 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
643 else
643 else
644 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
644 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
645 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
645 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
646 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
646 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
647 :id => wiki_page_id, :anchor => anchor, :parent => parent)
647 :id => wiki_page_id, :anchor => anchor, :parent => parent)
648 end
648 end
649 end
649 end
650 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
650 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
651 else
651 else
652 # project or wiki doesn't exist
652 # project or wiki doesn't exist
653 all
653 all
654 end
654 end
655 else
655 else
656 all
656 all
657 end
657 end
658 end
658 end
659 end
659 end
660
660
661 # Redmine links
661 # Redmine links
662 #
662 #
663 # Examples:
663 # Examples:
664 # Issues:
664 # Issues:
665 # #52 -> Link to issue #52
665 # #52 -> Link to issue #52
666 # Changesets:
666 # Changesets:
667 # r52 -> Link to revision 52
667 # r52 -> Link to revision 52
668 # commit:a85130f -> Link to scmid starting with a85130f
668 # commit:a85130f -> Link to scmid starting with a85130f
669 # Documents:
669 # Documents:
670 # document#17 -> Link to document with id 17
670 # document#17 -> Link to document with id 17
671 # document:Greetings -> Link to the document with title "Greetings"
671 # document:Greetings -> Link to the document with title "Greetings"
672 # document:"Some document" -> Link to the document with title "Some document"
672 # document:"Some document" -> Link to the document with title "Some document"
673 # Versions:
673 # Versions:
674 # version#3 -> Link to version with id 3
674 # version#3 -> Link to version with id 3
675 # version:1.0.0 -> Link to version named "1.0.0"
675 # version:1.0.0 -> Link to version named "1.0.0"
676 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
676 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
677 # Attachments:
677 # Attachments:
678 # attachment:file.zip -> Link to the attachment of the current object named file.zip
678 # attachment:file.zip -> Link to the attachment of the current object named file.zip
679 # Source files:
679 # Source files:
680 # source:some/file -> Link to the file located at /some/file in the project's repository
680 # source:some/file -> Link to the file located at /some/file in the project's repository
681 # source:some/file@52 -> Link to the file's revision 52
681 # source:some/file@52 -> Link to the file's revision 52
682 # source:some/file#L120 -> Link to line 120 of the file
682 # source:some/file#L120 -> Link to line 120 of the file
683 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
683 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
684 # export:some/file -> Force the download of the file
684 # export:some/file -> Force the download of the file
685 # Forum messages:
685 # Forum messages:
686 # message#1218 -> Link to message with id 1218
686 # message#1218 -> Link to message with id 1218
687 #
687 #
688 # Links can refer other objects from other projects, using project identifier:
688 # Links can refer other objects from other projects, using project identifier:
689 # identifier:r52
689 # identifier:r52
690 # identifier:document:"Some document"
690 # identifier:document:"Some document"
691 # identifier:version:1.0.0
691 # identifier:version:1.0.0
692 # identifier:source:some/file
692 # identifier:source:some/file
693 def parse_redmine_links(text, project, obj, attr, only_path, options)
693 def parse_redmine_links(text, project, obj, attr, only_path, options)
694 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
694 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
695 leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $2, $3, $4, $5, $10, $11, $8 || $12 || $18, $14 || $19, $15, $17
695 leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $2, $3, $4, $5, $10, $11, $8 || $12 || $18, $14 || $19, $15, $17
696 link = nil
696 link = nil
697 if project_identifier
697 if project_identifier
698 project = Project.visible.find_by_identifier(project_identifier)
698 project = Project.visible.find_by_identifier(project_identifier)
699 end
699 end
700 if esc.nil?
700 if esc.nil?
701 if prefix.nil? && sep == 'r'
701 if prefix.nil? && sep == 'r'
702 if project
702 if project
703 repository = nil
703 repository = nil
704 if repo_identifier
704 if repo_identifier
705 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
705 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
706 else
706 else
707 repository = project.repository
707 repository = project.repository
708 end
708 end
709 # project.changesets.visible raises an SQL error because of a double join on repositories
709 # project.changesets.visible raises an SQL error because of a double join on repositories
710 if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier))
710 if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier))
711 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.revision},
711 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.revision},
712 :class => 'changeset',
712 :class => 'changeset',
713 :title => truncate_single_line(changeset.comments, :length => 100))
713 :title => truncate_single_line(changeset.comments, :length => 100))
714 end
714 end
715 end
715 end
716 elsif sep == '#'
716 elsif sep == '#'
717 oid = identifier.to_i
717 oid = identifier.to_i
718 case prefix
718 case prefix
719 when nil
719 when nil
720 if oid.to_s == identifier && issue = Issue.visible.find_by_id(oid, :include => :status)
720 if oid.to_s == identifier && issue = Issue.visible.find_by_id(oid, :include => :status)
721 anchor = comment_id ? "note-#{comment_id}" : nil
721 anchor = comment_id ? "note-#{comment_id}" : nil
722 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor},
722 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor},
723 :class => issue.css_classes,
723 :class => issue.css_classes,
724 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
724 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
725 end
725 end
726 when 'document'
726 when 'document'
727 if document = Document.visible.find_by_id(oid)
727 if document = Document.visible.find_by_id(oid)
728 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
728 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
729 :class => 'document'
729 :class => 'document'
730 end
730 end
731 when 'version'
731 when 'version'
732 if version = Version.visible.find_by_id(oid)
732 if version = Version.visible.find_by_id(oid)
733 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
733 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
734 :class => 'version'
734 :class => 'version'
735 end
735 end
736 when 'message'
736 when 'message'
737 if message = Message.visible.find_by_id(oid, :include => :parent)
737 if message = Message.visible.find_by_id(oid, :include => :parent)
738 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
738 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
739 end
739 end
740 when 'forum'
740 when 'forum'
741 if board = Board.visible.find_by_id(oid)
741 if board = Board.visible.find_by_id(oid)
742 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
742 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
743 :class => 'board'
743 :class => 'board'
744 end
744 end
745 when 'news'
745 when 'news'
746 if news = News.visible.find_by_id(oid)
746 if news = News.visible.find_by_id(oid)
747 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
747 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
748 :class => 'news'
748 :class => 'news'
749 end
749 end
750 when 'project'
750 when 'project'
751 if p = Project.visible.find_by_id(oid)
751 if p = Project.visible.find_by_id(oid)
752 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
752 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
753 end
753 end
754 end
754 end
755 elsif sep == ':'
755 elsif sep == ':'
756 # removes the double quotes if any
756 # removes the double quotes if any
757 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
757 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
758 case prefix
758 case prefix
759 when 'document'
759 when 'document'
760 if project && document = project.documents.visible.find_by_title(name)
760 if project && document = project.documents.visible.find_by_title(name)
761 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
761 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
762 :class => 'document'
762 :class => 'document'
763 end
763 end
764 when 'version'
764 when 'version'
765 if project && version = project.versions.visible.find_by_name(name)
765 if project && version = project.versions.visible.find_by_name(name)
766 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
766 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
767 :class => 'version'
767 :class => 'version'
768 end
768 end
769 when 'forum'
769 when 'forum'
770 if project && board = project.boards.visible.find_by_name(name)
770 if project && board = project.boards.visible.find_by_name(name)
771 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
771 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
772 :class => 'board'
772 :class => 'board'
773 end
773 end
774 when 'news'
774 when 'news'
775 if project && news = project.news.visible.find_by_title(name)
775 if project && news = project.news.visible.find_by_title(name)
776 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
776 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
777 :class => 'news'
777 :class => 'news'
778 end
778 end
779 when 'commit', 'source', 'export'
779 when 'commit', 'source', 'export'
780 if project
780 if project
781 repository = nil
781 repository = nil
782 if name =~ %r{^(([a-z0-9\-]+)\|)(.+)$}
782 if name =~ %r{^(([a-z0-9\-]+)\|)(.+)$}
783 repo_prefix, repo_identifier, name = $1, $2, $3
783 repo_prefix, repo_identifier, name = $1, $2, $3
784 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
784 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
785 else
785 else
786 repository = project.repository
786 repository = project.repository
787 end
787 end
788 if prefix == 'commit'
788 if prefix == 'commit'
789 if repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%"]))
789 if repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%"]))
790 link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
790 link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
791 :class => 'changeset',
791 :class => 'changeset',
792 :title => truncate_single_line(h(changeset.comments), :length => 100)
792 :title => truncate_single_line(h(changeset.comments), :length => 100)
793 end
793 end
794 else
794 else
795 if repository && User.current.allowed_to?(:browse_repository, project)
795 if repository && User.current.allowed_to?(:browse_repository, project)
796 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
796 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
797 path, rev, anchor = $1, $3, $5
797 path, rev, anchor = $1, $3, $5
798 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => 'entry', :id => project, :repository_id => repository.identifier_param,
798 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
799 :path => to_path_param(path),
799 :path => to_path_param(path),
800 :rev => rev,
800 :rev => rev,
801 :anchor => anchor,
801 :anchor => anchor},
802 :format => (prefix == 'export' ? 'raw' : nil)},
803 :class => (prefix == 'export' ? 'source download' : 'source')
802 :class => (prefix == 'export' ? 'source download' : 'source')
804 end
803 end
805 end
804 end
806 repo_prefix = nil
805 repo_prefix = nil
807 end
806 end
808 when 'attachment'
807 when 'attachment'
809 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
808 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
810 if attachments && attachment = attachments.detect {|a| a.filename == name }
809 if attachments && attachment = attachments.detect {|a| a.filename == name }
811 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
810 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
812 :class => 'attachment'
811 :class => 'attachment'
813 end
812 end
814 when 'project'
813 when 'project'
815 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
814 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
816 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
815 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
817 end
816 end
818 end
817 end
819 end
818 end
820 end
819 end
821 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
820 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
822 end
821 end
823 end
822 end
824
823
825 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
824 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
826
825
827 def parse_sections(text, project, obj, attr, only_path, options)
826 def parse_sections(text, project, obj, attr, only_path, options)
828 return unless options[:edit_section_links]
827 return unless options[:edit_section_links]
829 text.gsub!(HEADING_RE) do
828 text.gsub!(HEADING_RE) do
830 heading = $1
829 heading = $1
831 @current_section += 1
830 @current_section += 1
832 if @current_section > 1
831 if @current_section > 1
833 content_tag('div',
832 content_tag('div',
834 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
833 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
835 :class => 'contextual',
834 :class => 'contextual',
836 :title => l(:button_edit_section)) + heading.html_safe
835 :title => l(:button_edit_section)) + heading.html_safe
837 else
836 else
838 heading
837 heading
839 end
838 end
840 end
839 end
841 end
840 end
842
841
843 # Headings and TOC
842 # Headings and TOC
844 # Adds ids and links to headings unless options[:headings] is set to false
843 # Adds ids and links to headings unless options[:headings] is set to false
845 def parse_headings(text, project, obj, attr, only_path, options)
844 def parse_headings(text, project, obj, attr, only_path, options)
846 return if options[:headings] == false
845 return if options[:headings] == false
847
846
848 text.gsub!(HEADING_RE) do
847 text.gsub!(HEADING_RE) do
849 level, attrs, content = $2.to_i, $3, $4
848 level, attrs, content = $2.to_i, $3, $4
850 item = strip_tags(content).strip
849 item = strip_tags(content).strip
851 anchor = sanitize_anchor_name(item)
850 anchor = sanitize_anchor_name(item)
852 # used for single-file wiki export
851 # used for single-file wiki export
853 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
852 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
854 @heading_anchors[anchor] ||= 0
853 @heading_anchors[anchor] ||= 0
855 idx = (@heading_anchors[anchor] += 1)
854 idx = (@heading_anchors[anchor] += 1)
856 if idx > 1
855 if idx > 1
857 anchor = "#{anchor}-#{idx}"
856 anchor = "#{anchor}-#{idx}"
858 end
857 end
859 @parsed_headings << [level, anchor, item]
858 @parsed_headings << [level, anchor, item]
860 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
859 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
861 end
860 end
862 end
861 end
863
862
864 MACROS_RE = /(
863 MACROS_RE = /(
865 (!)? # escaping
864 (!)? # escaping
866 (
865 (
867 \{\{ # opening tag
866 \{\{ # opening tag
868 ([\w]+) # macro name
867 ([\w]+) # macro name
869 (\(([^\n\r]*?)\))? # optional arguments
868 (\(([^\n\r]*?)\))? # optional arguments
870 ([\n\r].*?[\n\r])? # optional block of text
869 ([\n\r].*?[\n\r])? # optional block of text
871 \}\} # closing tag
870 \}\} # closing tag
872 )
871 )
873 )/mx unless const_defined?(:MACROS_RE)
872 )/mx unless const_defined?(:MACROS_RE)
874
873
875 MACRO_SUB_RE = /(
874 MACRO_SUB_RE = /(
876 \{\{
875 \{\{
877 macro\((\d+)\)
876 macro\((\d+)\)
878 \}\}
877 \}\}
879 )/x unless const_defined?(:MACRO_SUB_RE)
878 )/x unless const_defined?(:MACRO_SUB_RE)
880
879
881 # Extracts macros from text
880 # Extracts macros from text
882 def catch_macros(text)
881 def catch_macros(text)
883 macros = {}
882 macros = {}
884 text.gsub!(MACROS_RE) do
883 text.gsub!(MACROS_RE) do
885 all, macro = $1, $4.downcase
884 all, macro = $1, $4.downcase
886 if macro_exists?(macro) || all =~ MACRO_SUB_RE
885 if macro_exists?(macro) || all =~ MACRO_SUB_RE
887 index = macros.size
886 index = macros.size
888 macros[index] = all
887 macros[index] = all
889 "{{macro(#{index})}}"
888 "{{macro(#{index})}}"
890 else
889 else
891 all
890 all
892 end
891 end
893 end
892 end
894 macros
893 macros
895 end
894 end
896
895
897 # Executes and replaces macros in text
896 # Executes and replaces macros in text
898 def inject_macros(text, obj, macros, execute=true)
897 def inject_macros(text, obj, macros, execute=true)
899 text.gsub!(MACRO_SUB_RE) do
898 text.gsub!(MACRO_SUB_RE) do
900 all, index = $1, $2.to_i
899 all, index = $1, $2.to_i
901 orig = macros.delete(index)
900 orig = macros.delete(index)
902 if execute && orig && orig =~ MACROS_RE
901 if execute && orig && orig =~ MACROS_RE
903 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
902 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
904 if esc.nil?
903 if esc.nil?
905 h(exec_macro(macro, obj, args, block) || all)
904 h(exec_macro(macro, obj, args, block) || all)
906 else
905 else
907 h(all)
906 h(all)
908 end
907 end
909 elsif orig
908 elsif orig
910 h(orig)
909 h(orig)
911 else
910 else
912 h(all)
911 h(all)
913 end
912 end
914 end
913 end
915 end
914 end
916
915
917 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
916 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
918
917
919 # Renders the TOC with given headings
918 # Renders the TOC with given headings
920 def replace_toc(text, headings)
919 def replace_toc(text, headings)
921 text.gsub!(TOC_RE) do
920 text.gsub!(TOC_RE) do
922 # Keep only the 4 first levels
921 # Keep only the 4 first levels
923 headings = headings.select{|level, anchor, item| level <= 4}
922 headings = headings.select{|level, anchor, item| level <= 4}
924 if headings.empty?
923 if headings.empty?
925 ''
924 ''
926 else
925 else
927 div_class = 'toc'
926 div_class = 'toc'
928 div_class << ' right' if $1 == '>'
927 div_class << ' right' if $1 == '>'
929 div_class << ' left' if $1 == '<'
928 div_class << ' left' if $1 == '<'
930 out = "<ul class=\"#{div_class}\"><li>"
929 out = "<ul class=\"#{div_class}\"><li>"
931 root = headings.map(&:first).min
930 root = headings.map(&:first).min
932 current = root
931 current = root
933 started = false
932 started = false
934 headings.each do |level, anchor, item|
933 headings.each do |level, anchor, item|
935 if level > current
934 if level > current
936 out << '<ul><li>' * (level - current)
935 out << '<ul><li>' * (level - current)
937 elsif level < current
936 elsif level < current
938 out << "</li></ul>\n" * (current - level) + "</li><li>"
937 out << "</li></ul>\n" * (current - level) + "</li><li>"
939 elsif started
938 elsif started
940 out << '</li><li>'
939 out << '</li><li>'
941 end
940 end
942 out << "<a href=\"##{anchor}\">#{item}</a>"
941 out << "<a href=\"##{anchor}\">#{item}</a>"
943 current = level
942 current = level
944 started = true
943 started = true
945 end
944 end
946 out << '</li></ul>' * (current - root)
945 out << '</li></ul>' * (current - root)
947 out << '</li></ul>'
946 out << '</li></ul>'
948 end
947 end
949 end
948 end
950 end
949 end
951
950
952 # Same as Rails' simple_format helper without using paragraphs
951 # Same as Rails' simple_format helper without using paragraphs
953 def simple_format_without_paragraph(text)
952 def simple_format_without_paragraph(text)
954 text.to_s.
953 text.to_s.
955 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
954 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
956 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
955 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
957 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
956 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
958 html_safe
957 html_safe
959 end
958 end
960
959
961 def lang_options_for_select(blank=true)
960 def lang_options_for_select(blank=true)
962 (blank ? [["(auto)", ""]] : []) +
961 (blank ? [["(auto)", ""]] : []) +
963 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
962 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
964 end
963 end
965
964
966 def label_tag_for(name, option_tags = nil, options = {})
965 def label_tag_for(name, option_tags = nil, options = {})
967 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
966 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
968 content_tag("label", label_text)
967 content_tag("label", label_text)
969 end
968 end
970
969
971 def labelled_form_for(*args, &proc)
970 def labelled_form_for(*args, &proc)
972 args << {} unless args.last.is_a?(Hash)
971 args << {} unless args.last.is_a?(Hash)
973 options = args.last
972 options = args.last
974 if args.first.is_a?(Symbol)
973 if args.first.is_a?(Symbol)
975 options.merge!(:as => args.shift)
974 options.merge!(:as => args.shift)
976 end
975 end
977 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
976 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
978 form_for(*args, &proc)
977 form_for(*args, &proc)
979 end
978 end
980
979
981 def labelled_fields_for(*args, &proc)
980 def labelled_fields_for(*args, &proc)
982 args << {} unless args.last.is_a?(Hash)
981 args << {} unless args.last.is_a?(Hash)
983 options = args.last
982 options = args.last
984 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
983 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
985 fields_for(*args, &proc)
984 fields_for(*args, &proc)
986 end
985 end
987
986
988 def labelled_remote_form_for(*args, &proc)
987 def labelled_remote_form_for(*args, &proc)
989 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_remote_form_for is deprecated and will be removed in Redmine 2.2."
988 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_remote_form_for is deprecated and will be removed in Redmine 2.2."
990 args << {} unless args.last.is_a?(Hash)
989 args << {} unless args.last.is_a?(Hash)
991 options = args.last
990 options = args.last
992 options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
991 options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
993 form_for(*args, &proc)
992 form_for(*args, &proc)
994 end
993 end
995
994
996 def error_messages_for(*objects)
995 def error_messages_for(*objects)
997 html = ""
996 html = ""
998 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
997 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
999 errors = objects.map {|o| o.errors.full_messages}.flatten
998 errors = objects.map {|o| o.errors.full_messages}.flatten
1000 if errors.any?
999 if errors.any?
1001 html << "<div id='errorExplanation'><ul>\n"
1000 html << "<div id='errorExplanation'><ul>\n"
1002 errors.each do |error|
1001 errors.each do |error|
1003 html << "<li>#{h error}</li>\n"
1002 html << "<li>#{h error}</li>\n"
1004 end
1003 end
1005 html << "</ul></div>\n"
1004 html << "</ul></div>\n"
1006 end
1005 end
1007 html.html_safe
1006 html.html_safe
1008 end
1007 end
1009
1008
1010 def delete_link(url, options={})
1009 def delete_link(url, options={})
1011 options = {
1010 options = {
1012 :method => :delete,
1011 :method => :delete,
1013 :data => {:confirm => l(:text_are_you_sure)},
1012 :data => {:confirm => l(:text_are_you_sure)},
1014 :class => 'icon icon-del'
1013 :class => 'icon icon-del'
1015 }.merge(options)
1014 }.merge(options)
1016
1015
1017 link_to l(:button_delete), url, options
1016 link_to l(:button_delete), url, options
1018 end
1017 end
1019
1018
1020 def preview_link(url, form, target='preview', options={})
1019 def preview_link(url, form, target='preview', options={})
1021 content_tag 'a', l(:label_preview), {
1020 content_tag 'a', l(:label_preview), {
1022 :href => "#",
1021 :href => "#",
1023 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1022 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1024 :accesskey => accesskey(:preview)
1023 :accesskey => accesskey(:preview)
1025 }.merge(options)
1024 }.merge(options)
1026 end
1025 end
1027
1026
1028 def link_to_function(name, function, html_options={})
1027 def link_to_function(name, function, html_options={})
1029 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1028 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1030 end
1029 end
1031
1030
1032 # Helper to render JSON in views
1031 # Helper to render JSON in views
1033 def raw_json(arg)
1032 def raw_json(arg)
1034 arg.to_json.to_s.gsub('/', '\/').html_safe
1033 arg.to_json.to_s.gsub('/', '\/').html_safe
1035 end
1034 end
1036
1035
1037 def back_url
1036 def back_url
1038 url = params[:back_url]
1037 url = params[:back_url]
1039 if url.nil? && referer = request.env['HTTP_REFERER']
1038 if url.nil? && referer = request.env['HTTP_REFERER']
1040 url = CGI.unescape(referer.to_s)
1039 url = CGI.unescape(referer.to_s)
1041 end
1040 end
1042 url
1041 url
1043 end
1042 end
1044
1043
1045 def back_url_hidden_field_tag
1044 def back_url_hidden_field_tag
1046 url = back_url
1045 url = back_url
1047 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1046 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1048 end
1047 end
1049
1048
1050 def check_all_links(form_name)
1049 def check_all_links(form_name)
1051 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1050 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1052 " | ".html_safe +
1051 " | ".html_safe +
1053 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1052 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1054 end
1053 end
1055
1054
1056 def progress_bar(pcts, options={})
1055 def progress_bar(pcts, options={})
1057 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1056 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1058 pcts = pcts.collect(&:round)
1057 pcts = pcts.collect(&:round)
1059 pcts[1] = pcts[1] - pcts[0]
1058 pcts[1] = pcts[1] - pcts[0]
1060 pcts << (100 - pcts[1] - pcts[0])
1059 pcts << (100 - pcts[1] - pcts[0])
1061 width = options[:width] || '100px;'
1060 width = options[:width] || '100px;'
1062 legend = options[:legend] || ''
1061 legend = options[:legend] || ''
1063 content_tag('table',
1062 content_tag('table',
1064 content_tag('tr',
1063 content_tag('tr',
1065 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1064 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1066 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1065 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1067 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1066 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1068 ), :class => 'progress', :style => "width: #{width};").html_safe +
1067 ), :class => 'progress', :style => "width: #{width};").html_safe +
1069 content_tag('p', legend, :class => 'pourcent').html_safe
1068 content_tag('p', legend, :class => 'pourcent').html_safe
1070 end
1069 end
1071
1070
1072 def checked_image(checked=true)
1071 def checked_image(checked=true)
1073 if checked
1072 if checked
1074 image_tag 'toggle_check.png'
1073 image_tag 'toggle_check.png'
1075 end
1074 end
1076 end
1075 end
1077
1076
1078 def context_menu(url)
1077 def context_menu(url)
1079 unless @context_menu_included
1078 unless @context_menu_included
1080 content_for :header_tags do
1079 content_for :header_tags do
1081 javascript_include_tag('context_menu') +
1080 javascript_include_tag('context_menu') +
1082 stylesheet_link_tag('context_menu')
1081 stylesheet_link_tag('context_menu')
1083 end
1082 end
1084 if l(:direction) == 'rtl'
1083 if l(:direction) == 'rtl'
1085 content_for :header_tags do
1084 content_for :header_tags do
1086 stylesheet_link_tag('context_menu_rtl')
1085 stylesheet_link_tag('context_menu_rtl')
1087 end
1086 end
1088 end
1087 end
1089 @context_menu_included = true
1088 @context_menu_included = true
1090 end
1089 end
1091 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1090 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1092 end
1091 end
1093
1092
1094 def calendar_for(field_id)
1093 def calendar_for(field_id)
1095 include_calendar_headers_tags
1094 include_calendar_headers_tags
1096 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1095 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1097 end
1096 end
1098
1097
1099 def include_calendar_headers_tags
1098 def include_calendar_headers_tags
1100 unless @calendar_headers_tags_included
1099 unless @calendar_headers_tags_included
1101 @calendar_headers_tags_included = true
1100 @calendar_headers_tags_included = true
1102 content_for :header_tags do
1101 content_for :header_tags do
1103 start_of_week = Setting.start_of_week
1102 start_of_week = Setting.start_of_week
1104 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1103 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1105 # Redmine uses 1..7 (monday..sunday) in settings and locales
1104 # Redmine uses 1..7 (monday..sunday) in settings and locales
1106 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1105 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1107 start_of_week = start_of_week.to_i % 7
1106 start_of_week = start_of_week.to_i % 7
1108
1107
1109 tags = javascript_tag(
1108 tags = javascript_tag(
1110 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1109 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1111 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1110 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1112 path_to_image('/images/calendar.png') +
1111 path_to_image('/images/calendar.png') +
1113 "', showButtonPanel: true};")
1112 "', showButtonPanel: true};")
1114 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1113 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1115 unless jquery_locale == 'en'
1114 unless jquery_locale == 'en'
1116 tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js")
1115 tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js")
1117 end
1116 end
1118 tags
1117 tags
1119 end
1118 end
1120 end
1119 end
1121 end
1120 end
1122
1121
1123 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1122 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1124 # Examples:
1123 # Examples:
1125 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1124 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1126 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1125 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1127 #
1126 #
1128 def stylesheet_link_tag(*sources)
1127 def stylesheet_link_tag(*sources)
1129 options = sources.last.is_a?(Hash) ? sources.pop : {}
1128 options = sources.last.is_a?(Hash) ? sources.pop : {}
1130 plugin = options.delete(:plugin)
1129 plugin = options.delete(:plugin)
1131 sources = sources.map do |source|
1130 sources = sources.map do |source|
1132 if plugin
1131 if plugin
1133 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1132 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1134 elsif current_theme && current_theme.stylesheets.include?(source)
1133 elsif current_theme && current_theme.stylesheets.include?(source)
1135 current_theme.stylesheet_path(source)
1134 current_theme.stylesheet_path(source)
1136 else
1135 else
1137 source
1136 source
1138 end
1137 end
1139 end
1138 end
1140 super sources, options
1139 super sources, options
1141 end
1140 end
1142
1141
1143 # Overrides Rails' image_tag with themes and plugins support.
1142 # Overrides Rails' image_tag with themes and plugins support.
1144 # Examples:
1143 # Examples:
1145 # image_tag('image.png') # => picks image.png from the current theme or defaults
1144 # image_tag('image.png') # => picks image.png from the current theme or defaults
1146 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1145 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1147 #
1146 #
1148 def image_tag(source, options={})
1147 def image_tag(source, options={})
1149 if plugin = options.delete(:plugin)
1148 if plugin = options.delete(:plugin)
1150 source = "/plugin_assets/#{plugin}/images/#{source}"
1149 source = "/plugin_assets/#{plugin}/images/#{source}"
1151 elsif current_theme && current_theme.images.include?(source)
1150 elsif current_theme && current_theme.images.include?(source)
1152 source = current_theme.image_path(source)
1151 source = current_theme.image_path(source)
1153 end
1152 end
1154 super source, options
1153 super source, options
1155 end
1154 end
1156
1155
1157 # Overrides Rails' javascript_include_tag with plugins support
1156 # Overrides Rails' javascript_include_tag with plugins support
1158 # Examples:
1157 # Examples:
1159 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1158 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1160 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1159 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1161 #
1160 #
1162 def javascript_include_tag(*sources)
1161 def javascript_include_tag(*sources)
1163 options = sources.last.is_a?(Hash) ? sources.pop : {}
1162 options = sources.last.is_a?(Hash) ? sources.pop : {}
1164 if plugin = options.delete(:plugin)
1163 if plugin = options.delete(:plugin)
1165 sources = sources.map do |source|
1164 sources = sources.map do |source|
1166 if plugin
1165 if plugin
1167 "/plugin_assets/#{plugin}/javascripts/#{source}"
1166 "/plugin_assets/#{plugin}/javascripts/#{source}"
1168 else
1167 else
1169 source
1168 source
1170 end
1169 end
1171 end
1170 end
1172 end
1171 end
1173 super sources, options
1172 super sources, options
1174 end
1173 end
1175
1174
1176 def content_for(name, content = nil, &block)
1175 def content_for(name, content = nil, &block)
1177 @has_content ||= {}
1176 @has_content ||= {}
1178 @has_content[name] = true
1177 @has_content[name] = true
1179 super(name, content, &block)
1178 super(name, content, &block)
1180 end
1179 end
1181
1180
1182 def has_content?(name)
1181 def has_content?(name)
1183 (@has_content && @has_content[name]) || false
1182 (@has_content && @has_content[name]) || false
1184 end
1183 end
1185
1184
1186 def sidebar_content?
1185 def sidebar_content?
1187 has_content?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1186 has_content?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1188 end
1187 end
1189
1188
1190 def view_layouts_base_sidebar_hook_response
1189 def view_layouts_base_sidebar_hook_response
1191 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1190 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1192 end
1191 end
1193
1192
1194 def email_delivery_enabled?
1193 def email_delivery_enabled?
1195 !!ActionMailer::Base.perform_deliveries
1194 !!ActionMailer::Base.perform_deliveries
1196 end
1195 end
1197
1196
1198 # Returns the avatar image tag for the given +user+ if avatars are enabled
1197 # Returns the avatar image tag for the given +user+ if avatars are enabled
1199 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1198 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1200 def avatar(user, options = { })
1199 def avatar(user, options = { })
1201 if Setting.gravatar_enabled?
1200 if Setting.gravatar_enabled?
1202 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1201 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1203 email = nil
1202 email = nil
1204 if user.respond_to?(:mail)
1203 if user.respond_to?(:mail)
1205 email = user.mail
1204 email = user.mail
1206 elsif user.to_s =~ %r{<(.+?)>}
1205 elsif user.to_s =~ %r{<(.+?)>}
1207 email = $1
1206 email = $1
1208 end
1207 end
1209 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1208 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1210 else
1209 else
1211 ''
1210 ''
1212 end
1211 end
1213 end
1212 end
1214
1213
1215 def sanitize_anchor_name(anchor)
1214 def sanitize_anchor_name(anchor)
1216 if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java'
1215 if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java'
1217 anchor.gsub(%r{[^\p{Word}\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1216 anchor.gsub(%r{[^\p{Word}\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1218 else
1217 else
1219 # TODO: remove when ruby1.8 is no longer supported
1218 # TODO: remove when ruby1.8 is no longer supported
1220 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1219 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1221 end
1220 end
1222 end
1221 end
1223
1222
1224 # Returns the javascript tags that are included in the html layout head
1223 # Returns the javascript tags that are included in the html layout head
1225 def javascript_heads
1224 def javascript_heads
1226 tags = javascript_include_tag('jquery-1.7.2-ui-1.8.21-ujs-2.0.3', 'application')
1225 tags = javascript_include_tag('jquery-1.7.2-ui-1.8.21-ujs-2.0.3', 'application')
1227 unless User.current.pref.warn_on_leaving_unsaved == '0'
1226 unless User.current.pref.warn_on_leaving_unsaved == '0'
1228 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1227 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1229 end
1228 end
1230 tags
1229 tags
1231 end
1230 end
1232
1231
1233 def favicon
1232 def favicon
1234 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1233 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1235 end
1234 end
1236
1235
1237 def robot_exclusion_tag
1236 def robot_exclusion_tag
1238 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1237 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1239 end
1238 end
1240
1239
1241 # Returns true if arg is expected in the API response
1240 # Returns true if arg is expected in the API response
1242 def include_in_api_response?(arg)
1241 def include_in_api_response?(arg)
1243 unless @included_in_api_response
1242 unless @included_in_api_response
1244 param = params[:include]
1243 param = params[:include]
1245 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1244 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1246 @included_in_api_response.collect!(&:strip)
1245 @included_in_api_response.collect!(&:strip)
1247 end
1246 end
1248 @included_in_api_response.include?(arg.to_s)
1247 @included_in_api_response.include?(arg.to_s)
1249 end
1248 end
1250
1249
1251 # Returns options or nil if nometa param or X-Redmine-Nometa header
1250 # Returns options or nil if nometa param or X-Redmine-Nometa header
1252 # was set in the request
1251 # was set in the request
1253 def api_meta(options)
1252 def api_meta(options)
1254 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1253 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1255 # compatibility mode for activeresource clients that raise
1254 # compatibility mode for activeresource clients that raise
1256 # an error when unserializing an array with attributes
1255 # an error when unserializing an array with attributes
1257 nil
1256 nil
1258 else
1257 else
1259 options
1258 options
1260 end
1259 end
1261 end
1260 end
1262
1261
1263 private
1262 private
1264
1263
1265 def wiki_helper
1264 def wiki_helper
1266 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1265 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1267 extend helper
1266 extend helper
1268 return self
1267 return self
1269 end
1268 end
1270
1269
1271 def link_to_content_update(text, url_params = {}, html_options = {})
1270 def link_to_content_update(text, url_params = {}, html_options = {})
1272 link_to(text, url_params, html_options)
1271 link_to(text, url_params, html_options)
1273 end
1272 end
1274 end
1273 end
@@ -1,1122 +1,1133
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 require File.expand_path('../../../test_helper', __FILE__)
20 require File.expand_path('../../../test_helper', __FILE__)
21
21
22 class ApplicationHelperTest < ActionView::TestCase
22 class ApplicationHelperTest < ActionView::TestCase
23 include ERB::Util
23 include ERB::Util
24
24
25 fixtures :projects, :roles, :enabled_modules, :users,
25 fixtures :projects, :roles, :enabled_modules, :users,
26 :repositories, :changesets,
26 :repositories, :changesets,
27 :trackers, :issue_statuses, :issues, :versions, :documents,
27 :trackers, :issue_statuses, :issues, :versions, :documents,
28 :wikis, :wiki_pages, :wiki_contents,
28 :wikis, :wiki_pages, :wiki_contents,
29 :boards, :messages, :news,
29 :boards, :messages, :news,
30 :attachments, :enumerations
30 :attachments, :enumerations
31
31
32 def setup
32 def setup
33 super
33 super
34 set_tmp_attachments_directory
34 set_tmp_attachments_directory
35 end
35 end
36
36
37 context "#link_to_if_authorized" do
37 context "#link_to_if_authorized" do
38 context "authorized user" do
38 context "authorized user" do
39 should "be tested"
39 should "be tested"
40 end
40 end
41
41
42 context "unauthorized user" do
42 context "unauthorized user" do
43 should "be tested"
43 should "be tested"
44 end
44 end
45
45
46 should "allow using the :controller and :action for the target link" do
46 should "allow using the :controller and :action for the target link" do
47 User.current = User.find_by_login('admin')
47 User.current = User.find_by_login('admin')
48
48
49 @project = Issue.first.project # Used by helper
49 @project = Issue.first.project # Used by helper
50 response = link_to_if_authorized("By controller/action",
50 response = link_to_if_authorized("By controller/action",
51 {:controller => 'issues', :action => 'edit', :id => Issue.first.id})
51 {:controller => 'issues', :action => 'edit', :id => Issue.first.id})
52 assert_match /href/, response
52 assert_match /href/, response
53 end
53 end
54
54
55 end
55 end
56
56
57 def test_auto_links
57 def test_auto_links
58 to_test = {
58 to_test = {
59 'http://foo.bar' => '<a class="external" href="http://foo.bar">http://foo.bar</a>',
59 'http://foo.bar' => '<a class="external" href="http://foo.bar">http://foo.bar</a>',
60 'http://foo.bar/~user' => '<a class="external" href="http://foo.bar/~user">http://foo.bar/~user</a>',
60 'http://foo.bar/~user' => '<a class="external" href="http://foo.bar/~user">http://foo.bar/~user</a>',
61 'http://foo.bar.' => '<a class="external" href="http://foo.bar">http://foo.bar</a>.',
61 'http://foo.bar.' => '<a class="external" href="http://foo.bar">http://foo.bar</a>.',
62 'https://foo.bar.' => '<a class="external" href="https://foo.bar">https://foo.bar</a>.',
62 'https://foo.bar.' => '<a class="external" href="https://foo.bar">https://foo.bar</a>.',
63 'This is a link: http://foo.bar.' => 'This is a link: <a class="external" href="http://foo.bar">http://foo.bar</a>.',
63 'This is a link: http://foo.bar.' => 'This is a link: <a class="external" href="http://foo.bar">http://foo.bar</a>.',
64 'A link (eg. http://foo.bar).' => 'A link (eg. <a class="external" href="http://foo.bar">http://foo.bar</a>).',
64 'A link (eg. http://foo.bar).' => 'A link (eg. <a class="external" href="http://foo.bar">http://foo.bar</a>).',
65 '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>.',
65 '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>.',
66 'http://www.foo.bar/Test_(foobar)' => '<a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>',
66 'http://www.foo.bar/Test_(foobar)' => '<a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>',
67 '(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>)',
67 '(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>)',
68 '(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>)',
68 '(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>)',
69 '(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>).',
69 '(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>).',
70 '(see "inline link":http://www.foo.bar/Test_(foobar))' => '(see <a href="http://www.foo.bar/Test_(foobar)" class="external">inline link</a>)',
70 '(see "inline link":http://www.foo.bar/Test_(foobar))' => '(see <a href="http://www.foo.bar/Test_(foobar)" class="external">inline link</a>)',
71 '(see "inline link":http://www.foo.bar/Test)' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>)',
71 '(see "inline link":http://www.foo.bar/Test)' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>)',
72 '(see "inline link":http://www.foo.bar/Test).' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>).',
72 '(see "inline link":http://www.foo.bar/Test).' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>).',
73 'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>',
73 'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>',
74 '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>',
74 '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>',
75 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>',
75 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>',
76 'http://foo@www.bar.com' => '<a class="external" href="http://foo@www.bar.com">http://foo@www.bar.com</a>',
76 'http://foo@www.bar.com' => '<a class="external" href="http://foo@www.bar.com">http://foo@www.bar.com</a>',
77 'http://foo:bar@www.bar.com' => '<a class="external" href="http://foo:bar@www.bar.com">http://foo:bar@www.bar.com</a>',
77 'http://foo:bar@www.bar.com' => '<a class="external" href="http://foo:bar@www.bar.com">http://foo:bar@www.bar.com</a>',
78 'ftp://foo.bar' => '<a class="external" href="ftp://foo.bar">ftp://foo.bar</a>',
78 'ftp://foo.bar' => '<a class="external" href="ftp://foo.bar">ftp://foo.bar</a>',
79 'ftps://foo.bar' => '<a class="external" href="ftps://foo.bar">ftps://foo.bar</a>',
79 'ftps://foo.bar' => '<a class="external" href="ftps://foo.bar">ftps://foo.bar</a>',
80 'sftp://foo.bar' => '<a class="external" href="sftp://foo.bar">sftp://foo.bar</a>',
80 'sftp://foo.bar' => '<a class="external" href="sftp://foo.bar">sftp://foo.bar</a>',
81 # two exclamation marks
81 # two exclamation marks
82 '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>',
82 '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>',
83 # escaping
83 # escaping
84 'http://foo"bar' => '<a class="external" href="http://foo&quot;bar">http://foo&quot;bar</a>',
84 'http://foo"bar' => '<a class="external" href="http://foo&quot;bar">http://foo&quot;bar</a>',
85 # wrap in angle brackets
85 # wrap in angle brackets
86 '<http://foo.bar>' => '&lt;<a class="external" href="http://foo.bar">http://foo.bar</a>&gt;'
86 '<http://foo.bar>' => '&lt;<a class="external" href="http://foo.bar">http://foo.bar</a>&gt;'
87 }
87 }
88 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
88 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
89 end
89 end
90
90
91 def test_auto_mailto
91 def test_auto_mailto
92 assert_equal '<p><a class="email" href="mailto:test@foo.bar">test@foo.bar</a></p>',
92 assert_equal '<p><a class="email" href="mailto:test@foo.bar">test@foo.bar</a></p>',
93 textilizable('test@foo.bar')
93 textilizable('test@foo.bar')
94 end
94 end
95
95
96 def test_inline_images
96 def test_inline_images
97 to_test = {
97 to_test = {
98 '!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
98 '!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
99 'floating !>http://foo.bar/image.jpg!' => 'floating <div style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></div>',
99 'floating !>http://foo.bar/image.jpg!' => 'floating <div style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></div>',
100 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class <img src="http://foo.bar/image.jpg" class="some-class" alt="" />',
100 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class <img src="http://foo.bar/image.jpg" class="some-class" alt="" />',
101 'with style !{width:100px;height:100px}http://foo.bar/image.jpg!' => 'with style <img src="http://foo.bar/image.jpg" style="width:100px;height:100px;" alt="" />',
101 'with style !{width:100px;height:100px}http://foo.bar/image.jpg!' => 'with style <img src="http://foo.bar/image.jpg" style="width:100px;height:100px;" alt="" />',
102 '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" />',
102 '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" />',
103 '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;" />',
103 '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;" />',
104 }
104 }
105 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
105 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
106 end
106 end
107
107
108 def test_inline_images_inside_tags
108 def test_inline_images_inside_tags
109 raw = <<-RAW
109 raw = <<-RAW
110 h1. !foo.png! Heading
110 h1. !foo.png! Heading
111
111
112 Centered image:
112 Centered image:
113
113
114 p=. !bar.gif!
114 p=. !bar.gif!
115 RAW
115 RAW
116
116
117 assert textilizable(raw).include?('<img src="foo.png" alt="" />')
117 assert textilizable(raw).include?('<img src="foo.png" alt="" />')
118 assert textilizable(raw).include?('<img src="bar.gif" alt="" />')
118 assert textilizable(raw).include?('<img src="bar.gif" alt="" />')
119 end
119 end
120
120
121 def test_attached_images
121 def test_attached_images
122 to_test = {
122 to_test = {
123 'Inline image: !logo.gif!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
123 'Inline image: !logo.gif!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
124 'Inline image: !logo.GIF!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
124 'Inline image: !logo.GIF!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
125 'No match: !ogo.gif!' => 'No match: <img src="ogo.gif" alt="" />',
125 'No match: !ogo.gif!' => 'No match: <img src="ogo.gif" alt="" />',
126 'No match: !ogo.GIF!' => 'No match: <img src="ogo.GIF" alt="" />',
126 'No match: !ogo.GIF!' => 'No match: <img src="ogo.GIF" alt="" />',
127 # link image
127 # link image
128 '!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>',
128 '!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>',
129 }
129 }
130 attachments = Attachment.find(:all)
130 attachments = Attachment.find(:all)
131 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
131 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
132 end
132 end
133
133
134 def test_attached_images_filename_extension
134 def test_attached_images_filename_extension
135 set_tmp_attachments_directory
135 set_tmp_attachments_directory
136 a1 = Attachment.new(
136 a1 = Attachment.new(
137 :container => Issue.find(1),
137 :container => Issue.find(1),
138 :file => mock_file_with_options({:original_filename => "testtest.JPG"}),
138 :file => mock_file_with_options({:original_filename => "testtest.JPG"}),
139 :author => User.find(1))
139 :author => User.find(1))
140 assert a1.save
140 assert a1.save
141 assert_equal "testtest.JPG", a1.filename
141 assert_equal "testtest.JPG", a1.filename
142 assert_equal "image/jpeg", a1.content_type
142 assert_equal "image/jpeg", a1.content_type
143 assert a1.image?
143 assert a1.image?
144
144
145 a2 = Attachment.new(
145 a2 = Attachment.new(
146 :container => Issue.find(1),
146 :container => Issue.find(1),
147 :file => mock_file_with_options({:original_filename => "testtest.jpeg"}),
147 :file => mock_file_with_options({:original_filename => "testtest.jpeg"}),
148 :author => User.find(1))
148 :author => User.find(1))
149 assert a2.save
149 assert a2.save
150 assert_equal "testtest.jpeg", a2.filename
150 assert_equal "testtest.jpeg", a2.filename
151 assert_equal "image/jpeg", a2.content_type
151 assert_equal "image/jpeg", a2.content_type
152 assert a2.image?
152 assert a2.image?
153
153
154 a3 = Attachment.new(
154 a3 = Attachment.new(
155 :container => Issue.find(1),
155 :container => Issue.find(1),
156 :file => mock_file_with_options({:original_filename => "testtest.JPE"}),
156 :file => mock_file_with_options({:original_filename => "testtest.JPE"}),
157 :author => User.find(1))
157 :author => User.find(1))
158 assert a3.save
158 assert a3.save
159 assert_equal "testtest.JPE", a3.filename
159 assert_equal "testtest.JPE", a3.filename
160 assert_equal "image/jpeg", a3.content_type
160 assert_equal "image/jpeg", a3.content_type
161 assert a3.image?
161 assert a3.image?
162
162
163 a4 = Attachment.new(
163 a4 = Attachment.new(
164 :container => Issue.find(1),
164 :container => Issue.find(1),
165 :file => mock_file_with_options({:original_filename => "Testtest.BMP"}),
165 :file => mock_file_with_options({:original_filename => "Testtest.BMP"}),
166 :author => User.find(1))
166 :author => User.find(1))
167 assert a4.save
167 assert a4.save
168 assert_equal "Testtest.BMP", a4.filename
168 assert_equal "Testtest.BMP", a4.filename
169 assert_equal "image/x-ms-bmp", a4.content_type
169 assert_equal "image/x-ms-bmp", a4.content_type
170 assert a4.image?
170 assert a4.image?
171
171
172 to_test = {
172 to_test = {
173 'Inline image: !testtest.jpg!' =>
173 'Inline image: !testtest.jpg!' =>
174 'Inline image: <img src="/attachments/download/' + a1.id.to_s + '" alt="" />',
174 'Inline image: <img src="/attachments/download/' + a1.id.to_s + '" alt="" />',
175 'Inline image: !testtest.jpeg!' =>
175 'Inline image: !testtest.jpeg!' =>
176 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '" alt="" />',
176 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '" alt="" />',
177 'Inline image: !testtest.jpe!' =>
177 'Inline image: !testtest.jpe!' =>
178 'Inline image: <img src="/attachments/download/' + a3.id.to_s + '" alt="" />',
178 'Inline image: <img src="/attachments/download/' + a3.id.to_s + '" alt="" />',
179 'Inline image: !testtest.bmp!' =>
179 'Inline image: !testtest.bmp!' =>
180 'Inline image: <img src="/attachments/download/' + a4.id.to_s + '" alt="" />',
180 'Inline image: <img src="/attachments/download/' + a4.id.to_s + '" alt="" />',
181 }
181 }
182
182
183 attachments = [a1, a2, a3, a4]
183 attachments = [a1, a2, a3, a4]
184 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
184 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
185 end
185 end
186
186
187 def test_attached_images_should_read_later
187 def test_attached_images_should_read_later
188 set_fixtures_attachments_directory
188 set_fixtures_attachments_directory
189 a1 = Attachment.find(16)
189 a1 = Attachment.find(16)
190 assert_equal "testfile.png", a1.filename
190 assert_equal "testfile.png", a1.filename
191 assert a1.readable?
191 assert a1.readable?
192 assert (! a1.visible?(User.anonymous))
192 assert (! a1.visible?(User.anonymous))
193 assert a1.visible?(User.find(2))
193 assert a1.visible?(User.find(2))
194 a2 = Attachment.find(17)
194 a2 = Attachment.find(17)
195 assert_equal "testfile.PNG", a2.filename
195 assert_equal "testfile.PNG", a2.filename
196 assert a2.readable?
196 assert a2.readable?
197 assert (! a2.visible?(User.anonymous))
197 assert (! a2.visible?(User.anonymous))
198 assert a2.visible?(User.find(2))
198 assert a2.visible?(User.find(2))
199 assert a1.created_on < a2.created_on
199 assert a1.created_on < a2.created_on
200
200
201 to_test = {
201 to_test = {
202 'Inline image: !testfile.png!' =>
202 'Inline image: !testfile.png!' =>
203 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '" alt="" />',
203 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '" alt="" />',
204 'Inline image: !Testfile.PNG!' =>
204 'Inline image: !Testfile.PNG!' =>
205 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '" alt="" />',
205 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '" alt="" />',
206 }
206 }
207 attachments = [a1, a2]
207 attachments = [a1, a2]
208 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
208 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
209 set_tmp_attachments_directory
209 set_tmp_attachments_directory
210 end
210 end
211
211
212 def test_textile_external_links
212 def test_textile_external_links
213 to_test = {
213 to_test = {
214 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>',
214 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>',
215 'This is an intern "link":/foo/bar' => 'This is an intern <a href="/foo/bar">link</a>',
215 'This is an intern "link":/foo/bar' => 'This is an intern <a href="/foo/bar">link</a>',
216 '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>',
216 '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>',
217 '"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>',
217 '"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>',
218 "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":</p>\n\n\n\t<p>Another paragraph",
218 "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":</p>\n\n\n\t<p>Another paragraph",
219 # no multiline link text
219 # no multiline link text
220 "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",
220 "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",
221 # mailto link
221 # mailto link
222 "\"system administrator\":mailto:sysadmin@example.com?subject=redmine%20permissions" => "<a href=\"mailto:sysadmin@example.com?subject=redmine%20permissions\">system administrator</a>",
222 "\"system administrator\":mailto:sysadmin@example.com?subject=redmine%20permissions" => "<a href=\"mailto:sysadmin@example.com?subject=redmine%20permissions\">system administrator</a>",
223 # two exclamation marks
223 # two exclamation marks
224 '"a link":http://example.net/path!602815048C7B5C20!302.html' => '<a href="http://example.net/path!602815048C7B5C20!302.html" class="external">a link</a>',
224 '"a link":http://example.net/path!602815048C7B5C20!302.html' => '<a href="http://example.net/path!602815048C7B5C20!302.html" class="external">a link</a>',
225 # escaping
225 # escaping
226 '"test":http://foo"bar' => '<a href="http://foo&quot;bar" class="external">test</a>',
226 '"test":http://foo"bar' => '<a href="http://foo&quot;bar" class="external">test</a>',
227 }
227 }
228 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
228 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
229 end
229 end
230
230
231 def test_redmine_links
231 def test_redmine_links
232 issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3},
232 issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3},
233 :class => 'issue status-1 priority-4 overdue', :title => 'Error 281 when updating a recipe (New)')
233 :class => 'issue status-1 priority-4 overdue', :title => 'Error 281 when updating a recipe (New)')
234 note_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3, :anchor => 'note-14'},
234 note_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3, :anchor => 'note-14'},
235 :class => 'issue status-1 priority-4 overdue', :title => 'Error 281 when updating a recipe (New)')
235 :class => 'issue status-1 priority-4 overdue', :title => 'Error 281 when updating a recipe (New)')
236
236
237 changeset_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
237 changeset_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
238 :class => 'changeset', :title => 'My very first commit')
238 :class => 'changeset', :title => 'My very first commit')
239 changeset_link2 = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
239 changeset_link2 = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
240 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
240 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
241
241
242 document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
242 document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
243 :class => 'document')
243 :class => 'document')
244
244
245 version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
245 version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
246 :class => 'version')
246 :class => 'version')
247
247
248 board_url = {:controller => 'boards', :action => 'show', :id => 2, :project_id => 'ecookbook'}
248 board_url = {:controller => 'boards', :action => 'show', :id => 2, :project_id => 'ecookbook'}
249
249
250 message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4}
250 message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4}
251
251
252 news_url = {:controller => 'news', :action => 'show', :id => 1}
252 news_url = {:controller => 'news', :action => 'show', :id => 1}
253
253
254 project_url = {:controller => 'projects', :action => 'show', :id => 'subproject1'}
254 project_url = {:controller => 'projects', :action => 'show', :id => 'subproject1'}
255
255
256 source_url = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}
256 source_url = '/projects/ecookbook/repository/entry/some/file'
257 source_url_with_ext = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file.ext']}
257 source_url_with_rev = '/projects/ecookbook/repository/revisions/52/entry/some/file'
258 source_url_with_ext = '/projects/ecookbook/repository/entry/some/file.ext'
259 source_url_with_rev_and_ext = '/projects/ecookbook/repository/revisions/52/entry/some/file.ext'
260
261 export_url = '/projects/ecookbook/repository/raw/some/file'
262 export_url_with_rev = '/projects/ecookbook/repository/revisions/52/raw/some/file'
263 export_url_with_ext = '/projects/ecookbook/repository/raw/some/file.ext'
264 export_url_with_rev_and_ext = '/projects/ecookbook/repository/revisions/52/raw/some/file.ext'
258
265
259 to_test = {
266 to_test = {
260 # tickets
267 # tickets
261 '#3, [#3], (#3) and #3.' => "#{issue_link}, [#{issue_link}], (#{issue_link}) and #{issue_link}.",
268 '#3, [#3], (#3) and #3.' => "#{issue_link}, [#{issue_link}], (#{issue_link}) and #{issue_link}.",
262 # ticket notes
269 # ticket notes
263 '#3-14' => note_link,
270 '#3-14' => note_link,
264 '#3#note-14' => note_link,
271 '#3#note-14' => note_link,
265 # should not ignore leading zero
272 # should not ignore leading zero
266 '#03' => '#03',
273 '#03' => '#03',
267 # changesets
274 # changesets
268 'r1' => changeset_link,
275 'r1' => changeset_link,
269 'r1.' => "#{changeset_link}.",
276 'r1.' => "#{changeset_link}.",
270 'r1, r2' => "#{changeset_link}, #{changeset_link2}",
277 'r1, r2' => "#{changeset_link}, #{changeset_link2}",
271 'r1,r2' => "#{changeset_link},#{changeset_link2}",
278 'r1,r2' => "#{changeset_link},#{changeset_link2}",
272 # documents
279 # documents
273 'document#1' => document_link,
280 'document#1' => document_link,
274 'document:"Test document"' => document_link,
281 'document:"Test document"' => document_link,
275 # versions
282 # versions
276 'version#2' => version_link,
283 'version#2' => version_link,
277 'version:1.0' => version_link,
284 'version:1.0' => version_link,
278 'version:"1.0"' => version_link,
285 'version:"1.0"' => version_link,
279 # source
286 # source
280 'source:some/file' => link_to('source:some/file', source_url, :class => 'source'),
287 'source:some/file' => link_to('source:some/file', source_url, :class => 'source'),
281 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
288 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
282 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".",
289 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".",
283 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
290 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
284 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".",
291 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".",
285 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
292 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
286 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",",
293 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",",
287 'source:/some/file@52' => link_to('source:/some/file@52', source_url.merge(:rev => 52), :class => 'source'),
294 'source:/some/file@52' => link_to('source:/some/file@52', source_url_with_rev, :class => 'source'),
288 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_ext.merge(:rev => 52), :class => 'source'),
295 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_rev_and_ext, :class => 'source'),
289 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url.merge(:anchor => 'L110'), :class => 'source'),
296 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url + "#L110", :class => 'source'),
290 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext.merge(:anchor => 'L110'), :class => 'source'),
297 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext + "#L110", :class => 'source'),
291 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url.merge(:rev => 52, :anchor => 'L110'), :class => 'source'),
298 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url_with_rev + "#L110", :class => 'source'),
292 'export:/some/file' => link_to('export:/some/file', source_url.merge(:format => 'raw'), :class => 'source download'),
299 # export
300 'export:/some/file' => link_to('export:/some/file', export_url, :class => 'source download'),
301 'export:/some/file.ext' => link_to('export:/some/file.ext', export_url_with_ext, :class => 'source download'),
302 'export:/some/file@52' => link_to('export:/some/file@52', export_url_with_rev, :class => 'source download'),
303 'export:/some/file.ext@52' => link_to('export:/some/file.ext@52', export_url_with_rev_and_ext, :class => 'source download'),
293 # forum
304 # forum
294 'forum#2' => link_to('Discussion', board_url, :class => 'board'),
305 'forum#2' => link_to('Discussion', board_url, :class => 'board'),
295 'forum:Discussion' => link_to('Discussion', board_url, :class => 'board'),
306 'forum:Discussion' => link_to('Discussion', board_url, :class => 'board'),
296 # message
307 # message
297 'message#4' => link_to('Post 2', message_url, :class => 'message'),
308 'message#4' => link_to('Post 2', message_url, :class => 'message'),
298 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5', :r => 5), :class => 'message'),
309 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5', :r => 5), :class => 'message'),
299 # news
310 # news
300 'news#1' => link_to('eCookbook first release !', news_url, :class => 'news'),
311 'news#1' => link_to('eCookbook first release !', news_url, :class => 'news'),
301 'news:"eCookbook first release !"' => link_to('eCookbook first release !', news_url, :class => 'news'),
312 'news:"eCookbook first release !"' => link_to('eCookbook first release !', news_url, :class => 'news'),
302 # project
313 # project
303 'project#3' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
314 'project#3' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
304 'project:subproject1' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
315 'project:subproject1' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
305 'project:"eCookbook subProject 1"' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
316 'project:"eCookbook subProject 1"' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
306 # not found
317 # not found
307 '#0123456789' => '#0123456789',
318 '#0123456789' => '#0123456789',
308 # invalid expressions
319 # invalid expressions
309 'source:' => 'source:',
320 'source:' => 'source:',
310 # url hash
321 # url hash
311 "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
322 "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
312 }
323 }
313 @project = Project.find(1)
324 @project = Project.find(1)
314 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
325 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
315 end
326 end
316
327
317 def test_escaped_redmine_links_should_not_be_parsed
328 def test_escaped_redmine_links_should_not_be_parsed
318 to_test = [
329 to_test = [
319 '#3.',
330 '#3.',
320 '#3-14.',
331 '#3-14.',
321 '#3#-note14.',
332 '#3#-note14.',
322 'r1',
333 'r1',
323 'document#1',
334 'document#1',
324 'document:"Test document"',
335 'document:"Test document"',
325 'version#2',
336 'version#2',
326 'version:1.0',
337 'version:1.0',
327 'version:"1.0"',
338 'version:"1.0"',
328 'source:/some/file'
339 'source:/some/file'
329 ]
340 ]
330 @project = Project.find(1)
341 @project = Project.find(1)
331 to_test.each { |text| assert_equal "<p>#{text}</p>", textilizable("!" + text), "#{text} failed" }
342 to_test.each { |text| assert_equal "<p>#{text}</p>", textilizable("!" + text), "#{text} failed" }
332 end
343 end
333
344
334 def test_cross_project_redmine_links
345 def test_cross_project_redmine_links
335 source_link = link_to('ecookbook:source:/some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']},
346 source_link = link_to('ecookbook:source:/some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']},
336 :class => 'source')
347 :class => 'source')
337
348
338 changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
349 changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
339 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
350 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
340
351
341 to_test = {
352 to_test = {
342 # documents
353 # documents
343 'document:"Test document"' => 'document:"Test document"',
354 'document:"Test document"' => 'document:"Test document"',
344 'ecookbook:document:"Test document"' => '<a href="/documents/1" class="document">Test document</a>',
355 'ecookbook:document:"Test document"' => '<a href="/documents/1" class="document">Test document</a>',
345 'invalid:document:"Test document"' => 'invalid:document:"Test document"',
356 'invalid:document:"Test document"' => 'invalid:document:"Test document"',
346 # versions
357 # versions
347 'version:"1.0"' => 'version:"1.0"',
358 'version:"1.0"' => 'version:"1.0"',
348 'ecookbook:version:"1.0"' => '<a href="/versions/2" class="version">1.0</a>',
359 'ecookbook:version:"1.0"' => '<a href="/versions/2" class="version">1.0</a>',
349 'invalid:version:"1.0"' => 'invalid:version:"1.0"',
360 'invalid:version:"1.0"' => 'invalid:version:"1.0"',
350 # changeset
361 # changeset
351 'r2' => 'r2',
362 'r2' => 'r2',
352 'ecookbook:r2' => changeset_link,
363 'ecookbook:r2' => changeset_link,
353 'invalid:r2' => 'invalid:r2',
364 'invalid:r2' => 'invalid:r2',
354 # source
365 # source
355 'source:/some/file' => 'source:/some/file',
366 'source:/some/file' => 'source:/some/file',
356 'ecookbook:source:/some/file' => source_link,
367 'ecookbook:source:/some/file' => source_link,
357 'invalid:source:/some/file' => 'invalid:source:/some/file',
368 'invalid:source:/some/file' => 'invalid:source:/some/file',
358 }
369 }
359 @project = Project.find(3)
370 @project = Project.find(3)
360 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
371 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
361 end
372 end
362
373
363 def test_multiple_repositories_redmine_links
374 def test_multiple_repositories_redmine_links
364 svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn1', :url => 'file:///foo/hg')
375 svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn1', :url => 'file:///foo/hg')
365 Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
376 Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
366 hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
377 hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
367 Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
378 Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
368
379
369 changeset_link = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
380 changeset_link = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
370 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
381 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
371 svn_changeset_link = link_to('svn1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn1', :rev => 123},
382 svn_changeset_link = link_to('svn1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn1', :rev => 123},
372 :class => 'changeset', :title => '')
383 :class => 'changeset', :title => '')
373 hg_changeset_link = link_to('hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
384 hg_changeset_link = link_to('hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
374 :class => 'changeset', :title => '')
385 :class => 'changeset', :title => '')
375
386
376 source_link = link_to('source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
387 source_link = link_to('source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
377 hg_source_link = link_to('source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
388 hg_source_link = link_to('source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
378
389
379 to_test = {
390 to_test = {
380 'r2' => changeset_link,
391 'r2' => changeset_link,
381 'svn1|r123' => svn_changeset_link,
392 'svn1|r123' => svn_changeset_link,
382 'invalid|r123' => 'invalid|r123',
393 'invalid|r123' => 'invalid|r123',
383 'commit:hg1|abcd' => hg_changeset_link,
394 'commit:hg1|abcd' => hg_changeset_link,
384 'commit:invalid|abcd' => 'commit:invalid|abcd',
395 'commit:invalid|abcd' => 'commit:invalid|abcd',
385 # source
396 # source
386 'source:some/file' => source_link,
397 'source:some/file' => source_link,
387 'source:hg1|some/file' => hg_source_link,
398 'source:hg1|some/file' => hg_source_link,
388 'source:invalid|some/file' => 'source:invalid|some/file',
399 'source:invalid|some/file' => 'source:invalid|some/file',
389 }
400 }
390
401
391 @project = Project.find(1)
402 @project = Project.find(1)
392 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
403 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
393 end
404 end
394
405
395 def test_cross_project_multiple_repositories_redmine_links
406 def test_cross_project_multiple_repositories_redmine_links
396 svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn1', :url => 'file:///foo/hg')
407 svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn1', :url => 'file:///foo/hg')
397 Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
408 Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
398 hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
409 hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
399 Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
410 Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
400
411
401 changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
412 changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
402 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
413 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
403 svn_changeset_link = link_to('ecookbook:svn1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn1', :rev => 123},
414 svn_changeset_link = link_to('ecookbook:svn1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn1', :rev => 123},
404 :class => 'changeset', :title => '')
415 :class => 'changeset', :title => '')
405 hg_changeset_link = link_to('ecookbook:hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
416 hg_changeset_link = link_to('ecookbook:hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
406 :class => 'changeset', :title => '')
417 :class => 'changeset', :title => '')
407
418
408 source_link = link_to('ecookbook:source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
419 source_link = link_to('ecookbook:source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
409 hg_source_link = link_to('ecookbook:source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
420 hg_source_link = link_to('ecookbook:source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
410
421
411 to_test = {
422 to_test = {
412 'ecookbook:r2' => changeset_link,
423 'ecookbook:r2' => changeset_link,
413 'ecookbook:svn1|r123' => svn_changeset_link,
424 'ecookbook:svn1|r123' => svn_changeset_link,
414 'ecookbook:invalid|r123' => 'ecookbook:invalid|r123',
425 'ecookbook:invalid|r123' => 'ecookbook:invalid|r123',
415 'ecookbook:commit:hg1|abcd' => hg_changeset_link,
426 'ecookbook:commit:hg1|abcd' => hg_changeset_link,
416 'ecookbook:commit:invalid|abcd' => 'ecookbook:commit:invalid|abcd',
427 'ecookbook:commit:invalid|abcd' => 'ecookbook:commit:invalid|abcd',
417 'invalid:commit:invalid|abcd' => 'invalid:commit:invalid|abcd',
428 'invalid:commit:invalid|abcd' => 'invalid:commit:invalid|abcd',
418 # source
429 # source
419 'ecookbook:source:some/file' => source_link,
430 'ecookbook:source:some/file' => source_link,
420 'ecookbook:source:hg1|some/file' => hg_source_link,
431 'ecookbook:source:hg1|some/file' => hg_source_link,
421 'ecookbook:source:invalid|some/file' => 'ecookbook:source:invalid|some/file',
432 'ecookbook:source:invalid|some/file' => 'ecookbook:source:invalid|some/file',
422 'invalid:source:invalid|some/file' => 'invalid:source:invalid|some/file',
433 'invalid:source:invalid|some/file' => 'invalid:source:invalid|some/file',
423 }
434 }
424
435
425 @project = Project.find(3)
436 @project = Project.find(3)
426 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
437 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
427 end
438 end
428
439
429 def test_redmine_links_git_commit
440 def test_redmine_links_git_commit
430 changeset_link = link_to('abcd',
441 changeset_link = link_to('abcd',
431 {
442 {
432 :controller => 'repositories',
443 :controller => 'repositories',
433 :action => 'revision',
444 :action => 'revision',
434 :id => 'subproject1',
445 :id => 'subproject1',
435 :rev => 'abcd',
446 :rev => 'abcd',
436 },
447 },
437 :class => 'changeset', :title => 'test commit')
448 :class => 'changeset', :title => 'test commit')
438 to_test = {
449 to_test = {
439 'commit:abcd' => changeset_link,
450 'commit:abcd' => changeset_link,
440 }
451 }
441 @project = Project.find(3)
452 @project = Project.find(3)
442 r = Repository::Git.create!(:project => @project, :url => '/tmp/test/git')
453 r = Repository::Git.create!(:project => @project, :url => '/tmp/test/git')
443 assert r
454 assert r
444 c = Changeset.new(:repository => r,
455 c = Changeset.new(:repository => r,
445 :committed_on => Time.now,
456 :committed_on => Time.now,
446 :revision => 'abcd',
457 :revision => 'abcd',
447 :scmid => 'abcd',
458 :scmid => 'abcd',
448 :comments => 'test commit')
459 :comments => 'test commit')
449 assert( c.save )
460 assert( c.save )
450 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
461 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
451 end
462 end
452
463
453 # TODO: Bazaar commit id contains mail address, so it contains '@' and '_'.
464 # TODO: Bazaar commit id contains mail address, so it contains '@' and '_'.
454 def test_redmine_links_darcs_commit
465 def test_redmine_links_darcs_commit
455 changeset_link = link_to('20080308225258-98289-abcd456efg.gz',
466 changeset_link = link_to('20080308225258-98289-abcd456efg.gz',
456 {
467 {
457 :controller => 'repositories',
468 :controller => 'repositories',
458 :action => 'revision',
469 :action => 'revision',
459 :id => 'subproject1',
470 :id => 'subproject1',
460 :rev => '123',
471 :rev => '123',
461 },
472 },
462 :class => 'changeset', :title => 'test commit')
473 :class => 'changeset', :title => 'test commit')
463 to_test = {
474 to_test = {
464 'commit:20080308225258-98289-abcd456efg.gz' => changeset_link,
475 'commit:20080308225258-98289-abcd456efg.gz' => changeset_link,
465 }
476 }
466 @project = Project.find(3)
477 @project = Project.find(3)
467 r = Repository::Darcs.create!(
478 r = Repository::Darcs.create!(
468 :project => @project, :url => '/tmp/test/darcs',
479 :project => @project, :url => '/tmp/test/darcs',
469 :log_encoding => 'UTF-8')
480 :log_encoding => 'UTF-8')
470 assert r
481 assert r
471 c = Changeset.new(:repository => r,
482 c = Changeset.new(:repository => r,
472 :committed_on => Time.now,
483 :committed_on => Time.now,
473 :revision => '123',
484 :revision => '123',
474 :scmid => '20080308225258-98289-abcd456efg.gz',
485 :scmid => '20080308225258-98289-abcd456efg.gz',
475 :comments => 'test commit')
486 :comments => 'test commit')
476 assert( c.save )
487 assert( c.save )
477 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
488 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
478 end
489 end
479
490
480 def test_redmine_links_mercurial_commit
491 def test_redmine_links_mercurial_commit
481 changeset_link_rev = link_to('r123',
492 changeset_link_rev = link_to('r123',
482 {
493 {
483 :controller => 'repositories',
494 :controller => 'repositories',
484 :action => 'revision',
495 :action => 'revision',
485 :id => 'subproject1',
496 :id => 'subproject1',
486 :rev => '123' ,
497 :rev => '123' ,
487 },
498 },
488 :class => 'changeset', :title => 'test commit')
499 :class => 'changeset', :title => 'test commit')
489 changeset_link_commit = link_to('abcd',
500 changeset_link_commit = link_to('abcd',
490 {
501 {
491 :controller => 'repositories',
502 :controller => 'repositories',
492 :action => 'revision',
503 :action => 'revision',
493 :id => 'subproject1',
504 :id => 'subproject1',
494 :rev => 'abcd' ,
505 :rev => 'abcd' ,
495 },
506 },
496 :class => 'changeset', :title => 'test commit')
507 :class => 'changeset', :title => 'test commit')
497 to_test = {
508 to_test = {
498 'r123' => changeset_link_rev,
509 'r123' => changeset_link_rev,
499 'commit:abcd' => changeset_link_commit,
510 'commit:abcd' => changeset_link_commit,
500 }
511 }
501 @project = Project.find(3)
512 @project = Project.find(3)
502 r = Repository::Mercurial.create!(:project => @project, :url => '/tmp/test')
513 r = Repository::Mercurial.create!(:project => @project, :url => '/tmp/test')
503 assert r
514 assert r
504 c = Changeset.new(:repository => r,
515 c = Changeset.new(:repository => r,
505 :committed_on => Time.now,
516 :committed_on => Time.now,
506 :revision => '123',
517 :revision => '123',
507 :scmid => 'abcd',
518 :scmid => 'abcd',
508 :comments => 'test commit')
519 :comments => 'test commit')
509 assert( c.save )
520 assert( c.save )
510 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
521 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
511 end
522 end
512
523
513 def test_attachment_links
524 def test_attachment_links
514 attachment_link = link_to('error281.txt', {:controller => 'attachments', :action => 'download', :id => '1'}, :class => 'attachment')
525 attachment_link = link_to('error281.txt', {:controller => 'attachments', :action => 'download', :id => '1'}, :class => 'attachment')
515 to_test = {
526 to_test = {
516 'attachment:error281.txt' => attachment_link
527 'attachment:error281.txt' => attachment_link
517 }
528 }
518 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => Issue.find(3).attachments), "#{text} failed" }
529 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => Issue.find(3).attachments), "#{text} failed" }
519 end
530 end
520
531
521 def test_wiki_links
532 def test_wiki_links
522 to_test = {
533 to_test = {
523 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
534 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
524 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
535 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
525 # title content should be formatted
536 # title content should be formatted
526 '[[Another page|With _styled_ *title*]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">With <em>styled</em> <strong>title</strong></a>',
537 '[[Another page|With _styled_ *title*]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">With <em>styled</em> <strong>title</strong></a>',
527 '[[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>',
538 '[[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>',
528 # link with anchor
539 # link with anchor
529 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
540 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
530 '[[Another page#anchor|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page#anchor" class="wiki-page">Page</a>',
541 '[[Another page#anchor|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page#anchor" class="wiki-page">Page</a>',
531 # UTF8 anchor
542 # UTF8 anchor
532 '[[Another_page#ВСст|ВСст]]' => %|<a href="/projects/ecookbook/wiki/Another_page##{CGI.escape 'ВСст'}" class="wiki-page">ВСст</a>|,
543 '[[Another_page#ВСст|ВСст]]' => %|<a href="/projects/ecookbook/wiki/Another_page##{CGI.escape 'ВСст'}" class="wiki-page">ВСст</a>|,
533 # page that doesn't exist
544 # page that doesn't exist
534 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
545 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
535 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">404</a>',
546 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">404</a>',
536 # link to another project wiki
547 # link to another project wiki
537 '[[onlinestore:]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">onlinestore</a>',
548 '[[onlinestore:]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">onlinestore</a>',
538 '[[onlinestore:|Wiki]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">Wiki</a>',
549 '[[onlinestore:|Wiki]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">Wiki</a>',
539 '[[onlinestore:Start page]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Start page</a>',
550 '[[onlinestore:Start page]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Start page</a>',
540 '[[onlinestore:Start page|Text]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Text</a>',
551 '[[onlinestore:Start page|Text]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Text</a>',
541 '[[onlinestore:Unknown page]]' => '<a href="/projects/onlinestore/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
552 '[[onlinestore:Unknown page]]' => '<a href="/projects/onlinestore/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
542 # striked through link
553 # striked through link
543 '-[[Another page|Page]]-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a></del>',
554 '-[[Another page|Page]]-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a></del>',
544 '-[[Another page|Page]] link-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a> link</del>',
555 '-[[Another page|Page]] link-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a> link</del>',
545 # escaping
556 # escaping
546 '![[Another page|Page]]' => '[[Another page|Page]]',
557 '![[Another page|Page]]' => '[[Another page|Page]]',
547 # project does not exist
558 # project does not exist
548 '[[unknowproject:Start]]' => '[[unknowproject:Start]]',
559 '[[unknowproject:Start]]' => '[[unknowproject:Start]]',
549 '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]',
560 '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]',
550 }
561 }
551
562
552 @project = Project.find(1)
563 @project = Project.find(1)
553 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
564 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
554 end
565 end
555
566
556 def test_wiki_links_within_local_file_generation_context
567 def test_wiki_links_within_local_file_generation_context
557
568
558 to_test = {
569 to_test = {
559 # link to a page
570 # link to a page
560 '[[CookBook documentation]]' => '<a href="CookBook_documentation.html" class="wiki-page">CookBook documentation</a>',
571 '[[CookBook documentation]]' => '<a href="CookBook_documentation.html" class="wiki-page">CookBook documentation</a>',
561 '[[CookBook documentation|documentation]]' => '<a href="CookBook_documentation.html" class="wiki-page">documentation</a>',
572 '[[CookBook documentation|documentation]]' => '<a href="CookBook_documentation.html" class="wiki-page">documentation</a>',
562 '[[CookBook documentation#One-section]]' => '<a href="CookBook_documentation.html#One-section" class="wiki-page">CookBook documentation</a>',
573 '[[CookBook documentation#One-section]]' => '<a href="CookBook_documentation.html#One-section" class="wiki-page">CookBook documentation</a>',
563 '[[CookBook documentation#One-section|documentation]]' => '<a href="CookBook_documentation.html#One-section" class="wiki-page">documentation</a>',
574 '[[CookBook documentation#One-section|documentation]]' => '<a href="CookBook_documentation.html#One-section" class="wiki-page">documentation</a>',
564 # page that doesn't exist
575 # page that doesn't exist
565 '[[Unknown page]]' => '<a href="Unknown_page.html" class="wiki-page new">Unknown page</a>',
576 '[[Unknown page]]' => '<a href="Unknown_page.html" class="wiki-page new">Unknown page</a>',
566 '[[Unknown page|404]]' => '<a href="Unknown_page.html" class="wiki-page new">404</a>',
577 '[[Unknown page|404]]' => '<a href="Unknown_page.html" class="wiki-page new">404</a>',
567 '[[Unknown page#anchor]]' => '<a href="Unknown_page.html#anchor" class="wiki-page new">Unknown page</a>',
578 '[[Unknown page#anchor]]' => '<a href="Unknown_page.html#anchor" class="wiki-page new">Unknown page</a>',
568 '[[Unknown page#anchor|404]]' => '<a href="Unknown_page.html#anchor" class="wiki-page new">404</a>',
579 '[[Unknown page#anchor|404]]' => '<a href="Unknown_page.html#anchor" class="wiki-page new">404</a>',
569 }
580 }
570
581
571 @project = Project.find(1)
582 @project = Project.find(1)
572
583
573 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :local) }
584 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :local) }
574 end
585 end
575
586
576 def test_wiki_links_within_wiki_page_context
587 def test_wiki_links_within_wiki_page_context
577
588
578 page = WikiPage.find_by_title('Another_page' )
589 page = WikiPage.find_by_title('Another_page' )
579
590
580 to_test = {
591 to_test = {
581 # link to another page
592 # link to another page
582 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
593 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
583 '[[CookBook documentation|documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">documentation</a>',
594 '[[CookBook documentation|documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">documentation</a>',
584 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
595 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
585 '[[CookBook documentation#One-section|documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">documentation</a>',
596 '[[CookBook documentation#One-section|documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">documentation</a>',
586 # link to the current page
597 # link to the current page
587 '[[Another page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Another page</a>',
598 '[[Another page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Another page</a>',
588 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
599 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
589 '[[Another page#anchor]]' => '<a href="#anchor" class="wiki-page">Another page</a>',
600 '[[Another page#anchor]]' => '<a href="#anchor" class="wiki-page">Another page</a>',
590 '[[Another page#anchor|Page]]' => '<a href="#anchor" class="wiki-page">Page</a>',
601 '[[Another page#anchor|Page]]' => '<a href="#anchor" class="wiki-page">Page</a>',
591 # page that doesn't exist
602 # page that doesn't exist
592 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page" class="wiki-page new">Unknown page</a>',
603 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page" class="wiki-page new">Unknown page</a>',
593 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page" class="wiki-page new">404</a>',
604 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page" class="wiki-page new">404</a>',
594 '[[Unknown page#anchor]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor" class="wiki-page new">Unknown page</a>',
605 '[[Unknown page#anchor]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor" class="wiki-page new">Unknown page</a>',
595 '[[Unknown page#anchor|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor" class="wiki-page new">404</a>',
606 '[[Unknown page#anchor|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor" class="wiki-page new">404</a>',
596 }
607 }
597
608
598 @project = Project.find(1)
609 @project = Project.find(1)
599
610
600 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(WikiContent.new( :text => text, :page => page ), :text) }
611 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(WikiContent.new( :text => text, :page => page ), :text) }
601 end
612 end
602
613
603 def test_wiki_links_anchor_option_should_prepend_page_title_to_href
614 def test_wiki_links_anchor_option_should_prepend_page_title_to_href
604
615
605 to_test = {
616 to_test = {
606 # link to a page
617 # link to a page
607 '[[CookBook documentation]]' => '<a href="#CookBook_documentation" class="wiki-page">CookBook documentation</a>',
618 '[[CookBook documentation]]' => '<a href="#CookBook_documentation" class="wiki-page">CookBook documentation</a>',
608 '[[CookBook documentation|documentation]]' => '<a href="#CookBook_documentation" class="wiki-page">documentation</a>',
619 '[[CookBook documentation|documentation]]' => '<a href="#CookBook_documentation" class="wiki-page">documentation</a>',
609 '[[CookBook documentation#One-section]]' => '<a href="#CookBook_documentation_One-section" class="wiki-page">CookBook documentation</a>',
620 '[[CookBook documentation#One-section]]' => '<a href="#CookBook_documentation_One-section" class="wiki-page">CookBook documentation</a>',
610 '[[CookBook documentation#One-section|documentation]]' => '<a href="#CookBook_documentation_One-section" class="wiki-page">documentation</a>',
621 '[[CookBook documentation#One-section|documentation]]' => '<a href="#CookBook_documentation_One-section" class="wiki-page">documentation</a>',
611 # page that doesn't exist
622 # page that doesn't exist
612 '[[Unknown page]]' => '<a href="#Unknown_page" class="wiki-page new">Unknown page</a>',
623 '[[Unknown page]]' => '<a href="#Unknown_page" class="wiki-page new">Unknown page</a>',
613 '[[Unknown page|404]]' => '<a href="#Unknown_page" class="wiki-page new">404</a>',
624 '[[Unknown page|404]]' => '<a href="#Unknown_page" class="wiki-page new">404</a>',
614 '[[Unknown page#anchor]]' => '<a href="#Unknown_page_anchor" class="wiki-page new">Unknown page</a>',
625 '[[Unknown page#anchor]]' => '<a href="#Unknown_page_anchor" class="wiki-page new">Unknown page</a>',
615 '[[Unknown page#anchor|404]]' => '<a href="#Unknown_page_anchor" class="wiki-page new">404</a>',
626 '[[Unknown page#anchor|404]]' => '<a href="#Unknown_page_anchor" class="wiki-page new">404</a>',
616 }
627 }
617
628
618 @project = Project.find(1)
629 @project = Project.find(1)
619
630
620 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :anchor) }
631 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :anchor) }
621 end
632 end
622
633
623 def test_html_tags
634 def test_html_tags
624 to_test = {
635 to_test = {
625 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
636 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
626 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
637 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
627 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
638 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
628 # do not escape pre/code tags
639 # do not escape pre/code tags
629 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
640 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
630 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
641 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
631 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
642 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
632 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
643 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
633 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
644 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
634 # remove attributes except class
645 # remove attributes except class
635 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
646 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
636 '<pre class="foo">some text</pre>' => '<pre class="foo">some text</pre>',
647 '<pre class="foo">some text</pre>' => '<pre class="foo">some text</pre>',
637 "<pre class='foo bar'>some text</pre>" => "<pre class='foo bar'>some text</pre>",
648 "<pre class='foo bar'>some text</pre>" => "<pre class='foo bar'>some text</pre>",
638 '<pre class="foo bar">some text</pre>' => '<pre class="foo bar">some text</pre>',
649 '<pre class="foo bar">some text</pre>' => '<pre class="foo bar">some text</pre>',
639 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
650 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
640 # xss
651 # xss
641 '<pre><code class=""onmouseover="alert(1)">text</code></pre>' => '<pre><code>text</code></pre>',
652 '<pre><code class=""onmouseover="alert(1)">text</code></pre>' => '<pre><code>text</code></pre>',
642 '<pre class=""onmouseover="alert(1)">text</pre>' => '<pre>text</pre>',
653 '<pre class=""onmouseover="alert(1)">text</pre>' => '<pre>text</pre>',
643 }
654 }
644 to_test.each { |text, result| assert_equal result, textilizable(text) }
655 to_test.each { |text, result| assert_equal result, textilizable(text) }
645 end
656 end
646
657
647 def test_allowed_html_tags
658 def test_allowed_html_tags
648 to_test = {
659 to_test = {
649 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
660 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
650 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
661 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
651 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
662 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
652 }
663 }
653 to_test.each { |text, result| assert_equal result, textilizable(text) }
664 to_test.each { |text, result| assert_equal result, textilizable(text) }
654 end
665 end
655
666
656 def test_pre_tags
667 def test_pre_tags
657 raw = <<-RAW
668 raw = <<-RAW
658 Before
669 Before
659
670
660 <pre>
671 <pre>
661 <prepared-statement-cache-size>32</prepared-statement-cache-size>
672 <prepared-statement-cache-size>32</prepared-statement-cache-size>
662 </pre>
673 </pre>
663
674
664 After
675 After
665 RAW
676 RAW
666
677
667 expected = <<-EXPECTED
678 expected = <<-EXPECTED
668 <p>Before</p>
679 <p>Before</p>
669 <pre>
680 <pre>
670 &lt;prepared-statement-cache-size&gt;32&lt;/prepared-statement-cache-size&gt;
681 &lt;prepared-statement-cache-size&gt;32&lt;/prepared-statement-cache-size&gt;
671 </pre>
682 </pre>
672 <p>After</p>
683 <p>After</p>
673 EXPECTED
684 EXPECTED
674
685
675 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
686 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
676 end
687 end
677
688
678 def test_pre_content_should_not_parse_wiki_and_redmine_links
689 def test_pre_content_should_not_parse_wiki_and_redmine_links
679 raw = <<-RAW
690 raw = <<-RAW
680 [[CookBook documentation]]
691 [[CookBook documentation]]
681
692
682 #1
693 #1
683
694
684 <pre>
695 <pre>
685 [[CookBook documentation]]
696 [[CookBook documentation]]
686
697
687 #1
698 #1
688 </pre>
699 </pre>
689 RAW
700 RAW
690
701
691 expected = <<-EXPECTED
702 expected = <<-EXPECTED
692 <p><a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a></p>
703 <p><a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a></p>
693 <p><a href="/issues/1" class="issue status-1 priority-4" title="Can&#x27;t print recipes (New)">#1</a></p>
704 <p><a href="/issues/1" class="issue status-1 priority-4" title="Can&#x27;t print recipes (New)">#1</a></p>
694 <pre>
705 <pre>
695 [[CookBook documentation]]
706 [[CookBook documentation]]
696
707
697 #1
708 #1
698 </pre>
709 </pre>
699 EXPECTED
710 EXPECTED
700
711
701 @project = Project.find(1)
712 @project = Project.find(1)
702 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
713 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
703 end
714 end
704
715
705 def test_non_closing_pre_blocks_should_be_closed
716 def test_non_closing_pre_blocks_should_be_closed
706 raw = <<-RAW
717 raw = <<-RAW
707 <pre><code>
718 <pre><code>
708 RAW
719 RAW
709
720
710 expected = <<-EXPECTED
721 expected = <<-EXPECTED
711 <pre><code>
722 <pre><code>
712 </code></pre>
723 </code></pre>
713 EXPECTED
724 EXPECTED
714
725
715 @project = Project.find(1)
726 @project = Project.find(1)
716 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
727 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
717 end
728 end
718
729
719 def test_syntax_highlight
730 def test_syntax_highlight
720 raw = <<-RAW
731 raw = <<-RAW
721 <pre><code class="ruby">
732 <pre><code class="ruby">
722 # Some ruby code here
733 # Some ruby code here
723 </code></pre>
734 </code></pre>
724 RAW
735 RAW
725
736
726 expected = <<-EXPECTED
737 expected = <<-EXPECTED
727 <pre><code class="ruby syntaxhl"><span class=\"CodeRay\"><span class="comment"># Some ruby code here</span></span>
738 <pre><code class="ruby syntaxhl"><span class=\"CodeRay\"><span class="comment"># Some ruby code here</span></span>
728 </code></pre>
739 </code></pre>
729 EXPECTED
740 EXPECTED
730
741
731 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
742 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
732 end
743 end
733
744
734 def test_to_path_param
745 def test_to_path_param
735 assert_equal 'test1/test2', to_path_param('test1/test2')
746 assert_equal 'test1/test2', to_path_param('test1/test2')
736 assert_equal 'test1/test2', to_path_param('/test1/test2/')
747 assert_equal 'test1/test2', to_path_param('/test1/test2/')
737 assert_equal 'test1/test2', to_path_param('//test1/test2/')
748 assert_equal 'test1/test2', to_path_param('//test1/test2/')
738 assert_equal nil, to_path_param('/')
749 assert_equal nil, to_path_param('/')
739 end
750 end
740
751
741 def test_wiki_links_in_tables
752 def test_wiki_links_in_tables
742 to_test = {"|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" =>
753 to_test = {"|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" =>
743 '<tr><td><a href="/projects/ecookbook/wiki/Page" class="wiki-page new">Link title</a></td>' +
754 '<tr><td><a href="/projects/ecookbook/wiki/Page" class="wiki-page new">Link title</a></td>' +
744 '<td><a href="/projects/ecookbook/wiki/Other_Page" class="wiki-page new">Other title</a></td>' +
755 '<td><a href="/projects/ecookbook/wiki/Other_Page" class="wiki-page new">Other title</a></td>' +
745 '</tr><tr><td>Cell 21</td><td><a href="/projects/ecookbook/wiki/Last_page" class="wiki-page new">Last page</a></td></tr>'
756 '</tr><tr><td>Cell 21</td><td><a href="/projects/ecookbook/wiki/Last_page" class="wiki-page new">Last page</a></td></tr>'
746 }
757 }
747 @project = Project.find(1)
758 @project = Project.find(1)
748 to_test.each { |text, result| assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '') }
759 to_test.each { |text, result| assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '') }
749 end
760 end
750
761
751 def test_text_formatting
762 def test_text_formatting
752 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
763 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
753 '(_text within parentheses_)' => '(<em>text within parentheses</em>)',
764 '(_text within parentheses_)' => '(<em>text within parentheses</em>)',
754 'a *Humane Web* Text Generator' => 'a <strong>Humane Web</strong> Text Generator',
765 'a *Humane Web* Text Generator' => 'a <strong>Humane Web</strong> Text Generator',
755 '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>',
766 '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>',
756 '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',
767 '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',
757 }
768 }
758 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
769 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
759 end
770 end
760
771
761 def test_wiki_horizontal_rule
772 def test_wiki_horizontal_rule
762 assert_equal '<hr />', textilizable('---')
773 assert_equal '<hr />', textilizable('---')
763 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
774 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
764 end
775 end
765
776
766 def test_footnotes
777 def test_footnotes
767 raw = <<-RAW
778 raw = <<-RAW
768 This is some text[1].
779 This is some text[1].
769
780
770 fn1. This is the foot note
781 fn1. This is the foot note
771 RAW
782 RAW
772
783
773 expected = <<-EXPECTED
784 expected = <<-EXPECTED
774 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
785 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
775 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
786 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
776 EXPECTED
787 EXPECTED
777
788
778 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
789 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
779 end
790 end
780
791
781 def test_headings
792 def test_headings
782 raw = 'h1. Some heading'
793 raw = 'h1. Some heading'
783 expected = %|<a name="Some-heading"></a>\n<h1 >Some heading<a href="#Some-heading" class="wiki-anchor">&para;</a></h1>|
794 expected = %|<a name="Some-heading"></a>\n<h1 >Some heading<a href="#Some-heading" class="wiki-anchor">&para;</a></h1>|
784
795
785 assert_equal expected, textilizable(raw)
796 assert_equal expected, textilizable(raw)
786 end
797 end
787
798
788 def test_headings_with_special_chars
799 def test_headings_with_special_chars
789 # This test makes sure that the generated anchor names match the expected
800 # This test makes sure that the generated anchor names match the expected
790 # ones even if the heading text contains unconventional characters
801 # ones even if the heading text contains unconventional characters
791 raw = 'h1. Some heading related to version 0.5'
802 raw = 'h1. Some heading related to version 0.5'
792 anchor = sanitize_anchor_name("Some-heading-related-to-version-0.5")
803 anchor = sanitize_anchor_name("Some-heading-related-to-version-0.5")
793 expected = %|<a name="#{anchor}"></a>\n<h1 >Some heading related to version 0.5<a href="##{anchor}" class="wiki-anchor">&para;</a></h1>|
804 expected = %|<a name="#{anchor}"></a>\n<h1 >Some heading related to version 0.5<a href="##{anchor}" class="wiki-anchor">&para;</a></h1>|
794
805
795 assert_equal expected, textilizable(raw)
806 assert_equal expected, textilizable(raw)
796 end
807 end
797
808
798 def test_headings_in_wiki_single_page_export_should_be_prepended_with_page_title
809 def test_headings_in_wiki_single_page_export_should_be_prepended_with_page_title
799 page = WikiPage.new( :title => 'Page Title', :wiki_id => 1 )
810 page = WikiPage.new( :title => 'Page Title', :wiki_id => 1 )
800 content = WikiContent.new( :text => 'h1. Some heading', :page => page )
811 content = WikiContent.new( :text => 'h1. Some heading', :page => page )
801
812
802 expected = %|<a name="Page_Title_Some-heading"></a>\n<h1 >Some heading<a href="#Page_Title_Some-heading" class="wiki-anchor">&para;</a></h1>|
813 expected = %|<a name="Page_Title_Some-heading"></a>\n<h1 >Some heading<a href="#Page_Title_Some-heading" class="wiki-anchor">&para;</a></h1>|
803
814
804 assert_equal expected, textilizable(content, :text, :wiki_links => :anchor )
815 assert_equal expected, textilizable(content, :text, :wiki_links => :anchor )
805 end
816 end
806
817
807 def test_table_of_content
818 def test_table_of_content
808 raw = <<-RAW
819 raw = <<-RAW
809 {{toc}}
820 {{toc}}
810
821
811 h1. Title
822 h1. Title
812
823
813 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
824 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
814
825
815 h2. Subtitle with a [[Wiki]] link
826 h2. Subtitle with a [[Wiki]] link
816
827
817 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
828 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
818
829
819 h2. Subtitle with [[Wiki|another Wiki]] link
830 h2. Subtitle with [[Wiki|another Wiki]] link
820
831
821 h2. Subtitle with %{color:red}red text%
832 h2. Subtitle with %{color:red}red text%
822
833
823 <pre>
834 <pre>
824 some code
835 some code
825 </pre>
836 </pre>
826
837
827 h3. Subtitle with *some* _modifiers_
838 h3. Subtitle with *some* _modifiers_
828
839
829 h3. Subtitle with @inline code@
840 h3. Subtitle with @inline code@
830
841
831 h1. Another title
842 h1. Another title
832
843
833 h3. An "Internet link":http://www.redmine.org/ inside subtitle
844 h3. An "Internet link":http://www.redmine.org/ inside subtitle
834
845
835 h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
846 h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
836
847
837 RAW
848 RAW
838
849
839 expected = '<ul class="toc">' +
850 expected = '<ul class="toc">' +
840 '<li><a href="#Title">Title</a>' +
851 '<li><a href="#Title">Title</a>' +
841 '<ul>' +
852 '<ul>' +
842 '<li><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
853 '<li><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
843 '<li><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
854 '<li><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
844 '<li><a href="#Subtitle-with-red-text">Subtitle with red text</a>' +
855 '<li><a href="#Subtitle-with-red-text">Subtitle with red text</a>' +
845 '<ul>' +
856 '<ul>' +
846 '<li><a href="#Subtitle-with-some-modifiers">Subtitle with some modifiers</a></li>' +
857 '<li><a href="#Subtitle-with-some-modifiers">Subtitle with some modifiers</a></li>' +
847 '<li><a href="#Subtitle-with-inline-code">Subtitle with inline code</a></li>' +
858 '<li><a href="#Subtitle-with-inline-code">Subtitle with inline code</a></li>' +
848 '</ul>' +
859 '</ul>' +
849 '</li>' +
860 '</li>' +
850 '</ul>' +
861 '</ul>' +
851 '</li>' +
862 '</li>' +
852 '<li><a href="#Another-title">Another title</a>' +
863 '<li><a href="#Another-title">Another title</a>' +
853 '<ul>' +
864 '<ul>' +
854 '<li>' +
865 '<li>' +
855 '<ul>' +
866 '<ul>' +
856 '<li><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
867 '<li><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
857 '</ul>' +
868 '</ul>' +
858 '</li>' +
869 '</li>' +
859 '<li><a href="#Project-Name">Project Name</a></li>' +
870 '<li><a href="#Project-Name">Project Name</a></li>' +
860 '</ul>' +
871 '</ul>' +
861 '</li>' +
872 '</li>' +
862 '</ul>'
873 '</ul>'
863
874
864 @project = Project.find(1)
875 @project = Project.find(1)
865 assert textilizable(raw).gsub("\n", "").include?(expected)
876 assert textilizable(raw).gsub("\n", "").include?(expected)
866 end
877 end
867
878
868 def test_table_of_content_should_generate_unique_anchors
879 def test_table_of_content_should_generate_unique_anchors
869 raw = <<-RAW
880 raw = <<-RAW
870 {{toc}}
881 {{toc}}
871
882
872 h1. Title
883 h1. Title
873
884
874 h2. Subtitle
885 h2. Subtitle
875
886
876 h2. Subtitle
887 h2. Subtitle
877 RAW
888 RAW
878
889
879 expected = '<ul class="toc">' +
890 expected = '<ul class="toc">' +
880 '<li><a href="#Title">Title</a>' +
891 '<li><a href="#Title">Title</a>' +
881 '<ul>' +
892 '<ul>' +
882 '<li><a href="#Subtitle">Subtitle</a></li>' +
893 '<li><a href="#Subtitle">Subtitle</a></li>' +
883 '<li><a href="#Subtitle-2">Subtitle</a></li>'
894 '<li><a href="#Subtitle-2">Subtitle</a></li>'
884 '</ul>'
895 '</ul>'
885 '</li>' +
896 '</li>' +
886 '</ul>'
897 '</ul>'
887
898
888 @project = Project.find(1)
899 @project = Project.find(1)
889 result = textilizable(raw).gsub("\n", "")
900 result = textilizable(raw).gsub("\n", "")
890 assert_include expected, result
901 assert_include expected, result
891 assert_include '<a name="Subtitle">', result
902 assert_include '<a name="Subtitle">', result
892 assert_include '<a name="Subtitle-2">', result
903 assert_include '<a name="Subtitle-2">', result
893 end
904 end
894
905
895 def test_table_of_content_should_contain_included_page_headings
906 def test_table_of_content_should_contain_included_page_headings
896 raw = <<-RAW
907 raw = <<-RAW
897 {{toc}}
908 {{toc}}
898
909
899 h1. Included
910 h1. Included
900
911
901 {{include(Child_1)}}
912 {{include(Child_1)}}
902 RAW
913 RAW
903
914
904 expected = '<ul class="toc">' +
915 expected = '<ul class="toc">' +
905 '<li><a href="#Included">Included</a></li>' +
916 '<li><a href="#Included">Included</a></li>' +
906 '<li><a href="#Child-page-1">Child page 1</a></li>' +
917 '<li><a href="#Child-page-1">Child page 1</a></li>' +
907 '</ul>'
918 '</ul>'
908
919
909 @project = Project.find(1)
920 @project = Project.find(1)
910 assert textilizable(raw).gsub("\n", "").include?(expected)
921 assert textilizable(raw).gsub("\n", "").include?(expected)
911 end
922 end
912
923
913 def test_section_edit_links
924 def test_section_edit_links
914 raw = <<-RAW
925 raw = <<-RAW
915 h1. Title
926 h1. Title
916
927
917 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
928 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
918
929
919 h2. Subtitle with a [[Wiki]] link
930 h2. Subtitle with a [[Wiki]] link
920
931
921 h2. Subtitle with *some* _modifiers_
932 h2. Subtitle with *some* _modifiers_
922
933
923 h2. Subtitle with @inline code@
934 h2. Subtitle with @inline code@
924
935
925 <pre>
936 <pre>
926 some code
937 some code
927
938
928 h2. heading inside pre
939 h2. heading inside pre
929
940
930 <h2>html heading inside pre</h2>
941 <h2>html heading inside pre</h2>
931 </pre>
942 </pre>
932
943
933 h2. Subtitle after pre tag
944 h2. Subtitle after pre tag
934 RAW
945 RAW
935
946
936 @project = Project.find(1)
947 @project = Project.find(1)
937 set_language_if_valid 'en'
948 set_language_if_valid 'en'
938 result = textilizable(raw, :edit_section_links => {:controller => 'wiki', :action => 'edit', :project_id => '1', :id => 'Test'}).gsub("\n", "")
949 result = textilizable(raw, :edit_section_links => {:controller => 'wiki', :action => 'edit', :project_id => '1', :id => 'Test'}).gsub("\n", "")
939
950
940 # heading that contains inline code
951 # heading that contains inline code
941 assert_match Regexp.new('<div class="contextual" title="Edit this section">' +
952 assert_match Regexp.new('<div class="contextual" title="Edit this section">' +
942 '<a href="/projects/1/wiki/Test/edit\?section=4"><img alt="Edit" src="/images/edit.png(\?\d+)?" /></a></div>' +
953 '<a href="/projects/1/wiki/Test/edit\?section=4"><img alt="Edit" src="/images/edit.png(\?\d+)?" /></a></div>' +
943 '<a name="Subtitle-with-inline-code"></a>' +
954 '<a name="Subtitle-with-inline-code"></a>' +
944 '<h2 >Subtitle with <code>inline code</code><a href="#Subtitle-with-inline-code" class="wiki-anchor">&para;</a></h2>'),
955 '<h2 >Subtitle with <code>inline code</code><a href="#Subtitle-with-inline-code" class="wiki-anchor">&para;</a></h2>'),
945 result
956 result
946
957
947 # last heading
958 # last heading
948 assert_match Regexp.new('<div class="contextual" title="Edit this section">' +
959 assert_match Regexp.new('<div class="contextual" title="Edit this section">' +
949 '<a href="/projects/1/wiki/Test/edit\?section=5"><img alt="Edit" src="/images/edit.png(\?\d+)?" /></a></div>' +
960 '<a href="/projects/1/wiki/Test/edit\?section=5"><img alt="Edit" src="/images/edit.png(\?\d+)?" /></a></div>' +
950 '<a name="Subtitle-after-pre-tag"></a>' +
961 '<a name="Subtitle-after-pre-tag"></a>' +
951 '<h2 >Subtitle after pre tag<a href="#Subtitle-after-pre-tag" class="wiki-anchor">&para;</a></h2>'),
962 '<h2 >Subtitle after pre tag<a href="#Subtitle-after-pre-tag" class="wiki-anchor">&para;</a></h2>'),
952 result
963 result
953 end
964 end
954
965
955 def test_default_formatter
966 def test_default_formatter
956 with_settings :text_formatting => 'unknown' do
967 with_settings :text_formatting => 'unknown' do
957 text = 'a *link*: http://www.example.net/'
968 text = 'a *link*: http://www.example.net/'
958 assert_equal '<p>a *link*: <a class="external" href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
969 assert_equal '<p>a *link*: <a class="external" href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
959 end
970 end
960 end
971 end
961
972
962 def test_due_date_distance_in_words
973 def test_due_date_distance_in_words
963 to_test = { Date.today => 'Due in 0 days',
974 to_test = { Date.today => 'Due in 0 days',
964 Date.today + 1 => 'Due in 1 day',
975 Date.today + 1 => 'Due in 1 day',
965 Date.today + 100 => 'Due in about 3 months',
976 Date.today + 100 => 'Due in about 3 months',
966 Date.today + 20000 => 'Due in over 54 years',
977 Date.today + 20000 => 'Due in over 54 years',
967 Date.today - 1 => '1 day late',
978 Date.today - 1 => '1 day late',
968 Date.today - 100 => 'about 3 months late',
979 Date.today - 100 => 'about 3 months late',
969 Date.today - 20000 => 'over 54 years late',
980 Date.today - 20000 => 'over 54 years late',
970 }
981 }
971 ::I18n.locale = :en
982 ::I18n.locale = :en
972 to_test.each do |date, expected|
983 to_test.each do |date, expected|
973 assert_equal expected, due_date_distance_in_words(date)
984 assert_equal expected, due_date_distance_in_words(date)
974 end
985 end
975 end
986 end
976
987
977 def test_avatar_enabled
988 def test_avatar_enabled
978 with_settings :gravatar_enabled => '1' do
989 with_settings :gravatar_enabled => '1' do
979 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
990 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
980 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
991 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
981 # Default size is 50
992 # Default size is 50
982 assert avatar('jsmith <jsmith@somenet.foo>').include?('size=50')
993 assert avatar('jsmith <jsmith@somenet.foo>').include?('size=50')
983 assert avatar('jsmith <jsmith@somenet.foo>', :size => 24).include?('size=24')
994 assert avatar('jsmith <jsmith@somenet.foo>', :size => 24).include?('size=24')
984 # Non-avatar options should be considered html options
995 # Non-avatar options should be considered html options
985 assert avatar('jsmith <jsmith@somenet.foo>', :title => 'John Smith').include?('title="John Smith"')
996 assert avatar('jsmith <jsmith@somenet.foo>', :title => 'John Smith').include?('title="John Smith"')
986 # The default class of the img tag should be gravatar
997 # The default class of the img tag should be gravatar
987 assert avatar('jsmith <jsmith@somenet.foo>').include?('class="gravatar"')
998 assert avatar('jsmith <jsmith@somenet.foo>').include?('class="gravatar"')
988 assert !avatar('jsmith <jsmith@somenet.foo>', :class => 'picture').include?('class="gravatar"')
999 assert !avatar('jsmith <jsmith@somenet.foo>', :class => 'picture').include?('class="gravatar"')
989 assert_nil avatar('jsmith')
1000 assert_nil avatar('jsmith')
990 assert_nil avatar(nil)
1001 assert_nil avatar(nil)
991 end
1002 end
992 end
1003 end
993
1004
994 def test_avatar_disabled
1005 def test_avatar_disabled
995 with_settings :gravatar_enabled => '0' do
1006 with_settings :gravatar_enabled => '0' do
996 assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
1007 assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
997 end
1008 end
998 end
1009 end
999
1010
1000 def test_link_to_user
1011 def test_link_to_user
1001 user = User.find(2)
1012 user = User.find(2)
1002 t = link_to_user(user)
1013 t = link_to_user(user)
1003 assert_equal "<a href=\"/users/2\">#{ user.name }</a>", t
1014 assert_equal "<a href=\"/users/2\">#{ user.name }</a>", t
1004 end
1015 end
1005
1016
1006 def test_link_to_user_should_not_link_to_locked_user
1017 def test_link_to_user_should_not_link_to_locked_user
1007 user = User.find(5)
1018 user = User.find(5)
1008 assert user.locked?
1019 assert user.locked?
1009 t = link_to_user(user)
1020 t = link_to_user(user)
1010 assert_equal user.name, t
1021 assert_equal user.name, t
1011 end
1022 end
1012
1023
1013 def test_link_to_user_should_not_link_to_anonymous
1024 def test_link_to_user_should_not_link_to_anonymous
1014 user = User.anonymous
1025 user = User.anonymous
1015 assert user.anonymous?
1026 assert user.anonymous?
1016 t = link_to_user(user)
1027 t = link_to_user(user)
1017 assert_equal ::I18n.t(:label_user_anonymous), t
1028 assert_equal ::I18n.t(:label_user_anonymous), t
1018 end
1029 end
1019
1030
1020 def test_link_to_project
1031 def test_link_to_project
1021 project = Project.find(1)
1032 project = Project.find(1)
1022 assert_equal %(<a href="/projects/ecookbook">eCookbook</a>),
1033 assert_equal %(<a href="/projects/ecookbook">eCookbook</a>),
1023 link_to_project(project)
1034 link_to_project(project)
1024 assert_equal %(<a href="/projects/ecookbook/settings">eCookbook</a>),
1035 assert_equal %(<a href="/projects/ecookbook/settings">eCookbook</a>),
1025 link_to_project(project, :action => 'settings')
1036 link_to_project(project, :action => 'settings')
1026 assert_equal %(<a href="http://test.host/projects/ecookbook?jump=blah">eCookbook</a>),
1037 assert_equal %(<a href="http://test.host/projects/ecookbook?jump=blah">eCookbook</a>),
1027 link_to_project(project, {:only_path => false, :jump => 'blah'})
1038 link_to_project(project, {:only_path => false, :jump => 'blah'})
1028 assert_equal %(<a href="/projects/ecookbook/settings" class="project">eCookbook</a>),
1039 assert_equal %(<a href="/projects/ecookbook/settings" class="project">eCookbook</a>),
1029 link_to_project(project, {:action => 'settings'}, :class => "project")
1040 link_to_project(project, {:action => 'settings'}, :class => "project")
1030 end
1041 end
1031
1042
1032 def test_link_to_legacy_project_with_numerical_identifier_should_use_id
1043 def test_link_to_legacy_project_with_numerical_identifier_should_use_id
1033 # numeric identifier are no longer allowed
1044 # numeric identifier are no longer allowed
1034 Project.update_all "identifier=25", "id=1"
1045 Project.update_all "identifier=25", "id=1"
1035
1046
1036 assert_equal '<a href="/projects/1">eCookbook</a>',
1047 assert_equal '<a href="/projects/1">eCookbook</a>',
1037 link_to_project(Project.find(1))
1048 link_to_project(Project.find(1))
1038 end
1049 end
1039
1050
1040 def test_principals_options_for_select_with_users
1051 def test_principals_options_for_select_with_users
1041 User.current = nil
1052 User.current = nil
1042 users = [User.find(2), User.find(4)]
1053 users = [User.find(2), User.find(4)]
1043 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>),
1054 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>),
1044 principals_options_for_select(users)
1055 principals_options_for_select(users)
1045 end
1056 end
1046
1057
1047 def test_principals_options_for_select_with_selected
1058 def test_principals_options_for_select_with_selected
1048 User.current = nil
1059 User.current = nil
1049 users = [User.find(2), User.find(4)]
1060 users = [User.find(2), User.find(4)]
1050 assert_equal %(<option value="2">John Smith</option><option value="4" selected="selected">Robert Hill</option>),
1061 assert_equal %(<option value="2">John Smith</option><option value="4" selected="selected">Robert Hill</option>),
1051 principals_options_for_select(users, User.find(4))
1062 principals_options_for_select(users, User.find(4))
1052 end
1063 end
1053
1064
1054 def test_principals_options_for_select_with_users_and_groups
1065 def test_principals_options_for_select_with_users_and_groups
1055 User.current = nil
1066 User.current = nil
1056 users = [User.find(2), Group.find(11), User.find(4), Group.find(10)]
1067 users = [User.find(2), Group.find(11), User.find(4), Group.find(10)]
1057 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>) +
1068 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>) +
1058 %(<optgroup label="Groups"><option value="10">A Team</option><option value="11">B Team</option></optgroup>),
1069 %(<optgroup label="Groups"><option value="10">A Team</option><option value="11">B Team</option></optgroup>),
1059 principals_options_for_select(users)
1070 principals_options_for_select(users)
1060 end
1071 end
1061
1072
1062 def test_principals_options_for_select_with_empty_collection
1073 def test_principals_options_for_select_with_empty_collection
1063 assert_equal '', principals_options_for_select([])
1074 assert_equal '', principals_options_for_select([])
1064 end
1075 end
1065
1076
1066 def test_principals_options_for_select_should_include_me_option_when_current_user_is_in_collection
1077 def test_principals_options_for_select_should_include_me_option_when_current_user_is_in_collection
1067 users = [User.find(2), User.find(4)]
1078 users = [User.find(2), User.find(4)]
1068 User.current = User.find(4)
1079 User.current = User.find(4)
1069 assert_include '<option value="4">&lt;&lt; me &gt;&gt;</option>', principals_options_for_select(users)
1080 assert_include '<option value="4">&lt;&lt; me &gt;&gt;</option>', principals_options_for_select(users)
1070 end
1081 end
1071
1082
1072 def test_stylesheet_link_tag_should_pick_the_default_stylesheet
1083 def test_stylesheet_link_tag_should_pick_the_default_stylesheet
1073 assert_match 'href="/stylesheets/styles.css"', stylesheet_link_tag("styles")
1084 assert_match 'href="/stylesheets/styles.css"', stylesheet_link_tag("styles")
1074 end
1085 end
1075
1086
1076 def test_stylesheet_link_tag_for_plugin_should_pick_the_plugin_stylesheet
1087 def test_stylesheet_link_tag_for_plugin_should_pick_the_plugin_stylesheet
1077 assert_match 'href="/plugin_assets/foo/stylesheets/styles.css"', stylesheet_link_tag("styles", :plugin => :foo)
1088 assert_match 'href="/plugin_assets/foo/stylesheets/styles.css"', stylesheet_link_tag("styles", :plugin => :foo)
1078 end
1089 end
1079
1090
1080 def test_image_tag_should_pick_the_default_image
1091 def test_image_tag_should_pick_the_default_image
1081 assert_match 'src="/images/image.png"', image_tag("image.png")
1092 assert_match 'src="/images/image.png"', image_tag("image.png")
1082 end
1093 end
1083
1094
1084 def test_image_tag_should_pick_the_theme_image_if_it_exists
1095 def test_image_tag_should_pick_the_theme_image_if_it_exists
1085 theme = Redmine::Themes.themes.last
1096 theme = Redmine::Themes.themes.last
1086 theme.images << 'image.png'
1097 theme.images << 'image.png'
1087
1098
1088 with_settings :ui_theme => theme.id do
1099 with_settings :ui_theme => theme.id do
1089 assert_match %|src="/themes/#{theme.dir}/images/image.png"|, image_tag("image.png")
1100 assert_match %|src="/themes/#{theme.dir}/images/image.png"|, image_tag("image.png")
1090 assert_match %|src="/images/other.png"|, image_tag("other.png")
1101 assert_match %|src="/images/other.png"|, image_tag("other.png")
1091 end
1102 end
1092 ensure
1103 ensure
1093 theme.images.delete 'image.png'
1104 theme.images.delete 'image.png'
1094 end
1105 end
1095
1106
1096 def test_image_tag_sfor_plugin_should_pick_the_plugin_image
1107 def test_image_tag_sfor_plugin_should_pick_the_plugin_image
1097 assert_match 'src="/plugin_assets/foo/images/image.png"', image_tag("image.png", :plugin => :foo)
1108 assert_match 'src="/plugin_assets/foo/images/image.png"', image_tag("image.png", :plugin => :foo)
1098 end
1109 end
1099
1110
1100 def test_javascript_include_tag_should_pick_the_default_javascript
1111 def test_javascript_include_tag_should_pick_the_default_javascript
1101 assert_match 'src="/javascripts/scripts.js"', javascript_include_tag("scripts")
1112 assert_match 'src="/javascripts/scripts.js"', javascript_include_tag("scripts")
1102 end
1113 end
1103
1114
1104 def test_javascript_include_tag_for_plugin_should_pick_the_plugin_javascript
1115 def test_javascript_include_tag_for_plugin_should_pick_the_plugin_javascript
1105 assert_match 'src="/plugin_assets/foo/javascripts/scripts.js"', javascript_include_tag("scripts", :plugin => :foo)
1116 assert_match 'src="/plugin_assets/foo/javascripts/scripts.js"', javascript_include_tag("scripts", :plugin => :foo)
1106 end
1117 end
1107
1118
1108 def test_per_page_links_should_show_usefull_values
1119 def test_per_page_links_should_show_usefull_values
1109 set_language_if_valid 'en'
1120 set_language_if_valid 'en'
1110 stubs(:link_to).returns("[link]")
1121 stubs(:link_to).returns("[link]")
1111
1122
1112 with_settings :per_page_options => '10, 25, 50, 100' do
1123 with_settings :per_page_options => '10, 25, 50, 100' do
1113 assert_nil per_page_links(10, 3)
1124 assert_nil per_page_links(10, 3)
1114 assert_nil per_page_links(25, 3)
1125 assert_nil per_page_links(25, 3)
1115 assert_equal "Per page: 10, [link]", per_page_links(10, 22)
1126 assert_equal "Per page: 10, [link]", per_page_links(10, 22)
1116 assert_equal "Per page: [link], 25", per_page_links(25, 22)
1127 assert_equal "Per page: [link], 25", per_page_links(25, 22)
1117 assert_equal "Per page: [link], [link], 50", per_page_links(50, 22)
1128 assert_equal "Per page: [link], [link], 50", per_page_links(50, 22)
1118 assert_equal "Per page: [link], 25, [link]", per_page_links(25, 26)
1129 assert_equal "Per page: [link], 25, [link]", per_page_links(25, 26)
1119 assert_equal "Per page: [link], 25, [link], [link]", per_page_links(25, 120)
1130 assert_equal "Per page: [link], 25, [link], [link]", per_page_links(25, 120)
1120 end
1131 end
1121 end
1132 end
1122 end
1133 end
General Comments 0
You need to be logged in to leave comments. Login now