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