##// END OF EJS Templates
Extends child_pages macro to display child pages based on page parameter (#1975)....
Jean-Philippe Lang -
r2051:06266c8fecd7
parent child
Show More
@@ -0,0 +1,98
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
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
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 require File.dirname(__FILE__) + '/../../../../test_helper'
19
20 class Redmine::WikiFormatting::MacrosTest < HelperTestCase
21 include ApplicationHelper
22 include ActionView::Helpers::TextHelper
23 fixtures :projects, :roles, :enabled_modules, :users,
24 :repositories, :changesets,
25 :trackers, :issue_statuses, :issues,
26 :versions, :documents,
27 :wikis, :wiki_pages, :wiki_contents,
28 :boards, :messages,
29 :attachments
30
31 def setup
32 super
33 @project = nil
34 end
35
36 def teardown
37 end
38
39 def test_macro_hello_world
40 text = "{{hello_world}}"
41 assert textilizable(text).match(/Hello world!/)
42 # escaping
43 text = "!{{hello_world}}"
44 assert_equal '<p>{{hello_world}}</p>', textilizable(text)
45 end
46
47 def test_macro_include
48 @project = Project.find(1)
49 # include a page of the current project wiki
50 text = "{{include(Another page)}}"
51 assert textilizable(text).match(/This is a link to a ticket/)
52
53 @project = nil
54 # include a page of a specific project wiki
55 text = "{{include(ecookbook:Another page)}}"
56 assert textilizable(text).match(/This is a link to a ticket/)
57
58 text = "{{include(ecookbook:)}}"
59 assert textilizable(text).match(/CookBook documentation/)
60
61 text = "{{include(unknowidentifier:somepage)}}"
62 assert textilizable(text).match(/Page not found/)
63 end
64
65 def test_macro_child_pages
66 expected = "<p><ul class=\"pages-hierarchy\">\n" +
67 "<li><a href=\"/wiki/ecookbook/Child_1\">Child 1</a></li>\n" +
68 "<li><a href=\"/wiki/ecookbook/Child_2\">Child 2</a></li>\n" +
69 "</ul>\n</p>"
70
71 @project = Project.find(1)
72 # child pages of the current wiki page
73 assert_equal expected, textilizable("{{child_pages}}", :object => WikiPage.find(2).content)
74 # child pages of another page
75 assert_equal expected, textilizable("{{child_pages(Another_page)}}", :object => WikiPage.find(1).content)
76
77 @project = Project.find(2)
78 assert_equal expected, textilizable("{{child_pages(ecookbook:Another_page)}}", :object => WikiPage.find(1).content)
79 end
80
81 def test_macro_child_pages_with_option
82 expected = "<p><ul class=\"pages-hierarchy\">\n" +
83 "<li><a href=\"/wiki/ecookbook/Another_page\">Another page</a>\n" +
84 "<ul class=\"pages-hierarchy\">\n" +
85 "<li><a href=\"/wiki/ecookbook/Child_1\">Child 1</a></li>\n" +
86 "<li><a href=\"/wiki/ecookbook/Child_2\">Child 2</a></li>\n" +
87 "</ul>\n</li>\n</ul>\n</p>"
88
89 @project = Project.find(1)
90 # child pages of the current wiki page
91 assert_equal expected, textilizable("{{child_pages(parent=1)}}", :object => WikiPage.find(2).content)
92 # child pages of another page
93 assert_equal expected, textilizable("{{child_pages(Another_page, parent=1)}}", :object => WikiPage.find(1).content)
94
95 @project = Project.find(2)
96 assert_equal expected, textilizable("{{child_pages(ecookbook:Another_page, parent=1)}}", :object => WikiPage.find(1).content)
97 end
98 end
@@ -1,589 +1,605
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'coderay'
19 19 require 'coderay/helpers/file_type'
20 20 require 'forwardable'
21 21
22 22 module ApplicationHelper
23 23 include Redmine::WikiFormatting::Macros::Definitions
24 24 include GravatarHelper::PublicMethods
25 25
26 26 extend Forwardable
27 27 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
28 28
29 29 def current_role
30 30 @current_role ||= User.current.role_for_project(@project)
31 31 end
32 32
33 33 # Return true if user is authorized for controller/action, otherwise false
34 34 def authorize_for(controller, action)
35 35 User.current.allowed_to?({:controller => controller, :action => action}, @project)
36 36 end
37 37
38 38 # Display a link if user is authorized
39 39 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
40 40 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
41 41 end
42 42
43 43 # Display a link to remote if user is authorized
44 44 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
45 45 url = options[:url] || {}
46 46 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
47 47 end
48 48
49 49 # Display a link to user's account page
50 50 def link_to_user(user)
51 51 user ? link_to(user, :controller => 'account', :action => 'show', :id => user) : 'Anonymous'
52 52 end
53 53
54 54 def link_to_issue(issue, options={})
55 55 options[:class] ||= ''
56 56 options[:class] << ' issue'
57 57 options[:class] << ' closed' if issue.closed?
58 58 link_to "#{issue.tracker.name} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue}, options
59 59 end
60 60
61 61 # Generates a link to an attachment.
62 62 # Options:
63 63 # * :text - Link text (default to attachment filename)
64 64 # * :download - Force download (default: false)
65 65 def link_to_attachment(attachment, options={})
66 66 text = options.delete(:text) || attachment.filename
67 67 action = options.delete(:download) ? 'download' : 'show'
68 68
69 69 link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
70 70 end
71 71
72 72 def toggle_link(name, id, options={})
73 73 onclick = "Element.toggle('#{id}'); "
74 74 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
75 75 onclick << "return false;"
76 76 link_to(name, "#", :onclick => onclick)
77 77 end
78 78
79 79 def image_to_function(name, function, html_options = {})
80 80 html_options.symbolize_keys!
81 81 tag(:input, html_options.merge({
82 82 :type => "image", :src => image_path(name),
83 83 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
84 84 }))
85 85 end
86 86
87 87 def prompt_to_remote(name, text, param, url, html_options = {})
88 88 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
89 89 link_to name, {}, html_options
90 90 end
91 91
92 92 def format_date(date)
93 93 return nil unless date
94 94 # "Setting.date_format.size < 2" is a temporary fix (content of date_format setting changed)
95 95 @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
96 96 date.strftime(@date_format)
97 97 end
98 98
99 99 def format_time(time, include_date = true)
100 100 return nil unless time
101 101 time = time.to_time if time.is_a?(String)
102 102 zone = User.current.time_zone
103 103 local = zone ? time.in_time_zone(zone) : (time.utc? ? time.localtime : time)
104 104 @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
105 105 @time_format ||= (Setting.time_format.blank? ? l(:general_fmt_time) : Setting.time_format)
106 106 include_date ? local.strftime("#{@date_format} #{@time_format}") : local.strftime(@time_format)
107 107 end
108 108
109 109 def distance_of_date_in_words(from_date, to_date = 0)
110 110 from_date = from_date.to_date if from_date.respond_to?(:to_date)
111 111 to_date = to_date.to_date if to_date.respond_to?(:to_date)
112 112 distance_in_days = (to_date - from_date).abs
113 113 lwr(:actionview_datehelper_time_in_words_day, distance_in_days)
114 114 end
115 115
116 116 def due_date_distance_in_words(date)
117 117 if date
118 118 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
119 119 end
120 120 end
121 121
122 def render_page_hierarchy(pages, node=nil)
123 content = ''
124 if pages[node]
125 content << "<ul class=\"pages-hierarchy\">\n"
126 pages[node].each do |page|
127 content << "<li>"
128 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'index', :id => page.project, :page => page.title},
129 :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
130 content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
131 content << "</li>\n"
132 end
133 content << "</ul>\n"
134 end
135 content
136 end
137
122 138 # Truncates and returns the string as a single line
123 139 def truncate_single_line(string, *args)
124 140 truncate(string, *args).gsub(%r{[\r\n]+}m, ' ')
125 141 end
126 142
127 143 def html_hours(text)
128 144 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
129 145 end
130 146
131 147 def authoring(created, author)
132 148 time_tag = @project.nil? ? content_tag('acronym', distance_of_time_in_words(Time.now, created), :title => format_time(created)) :
133 149 link_to(distance_of_time_in_words(Time.now, created),
134 150 {:controller => 'projects', :action => 'activity', :id => @project, :from => created.to_date},
135 151 :title => format_time(created))
136 152 author_tag = (author.is_a?(User) && !author.anonymous?) ? link_to(h(author), :controller => 'account', :action => 'show', :id => author) : h(author || 'Anonymous')
137 153 l(:label_added_time_by, author_tag, time_tag)
138 154 end
139 155
140 156 def l_or_humanize(s, options={})
141 157 k = "#{options[:prefix]}#{s}".to_sym
142 158 l_has_string?(k) ? l(k) : s.to_s.humanize
143 159 end
144 160
145 161 def day_name(day)
146 162 l(:general_day_names).split(',')[day-1]
147 163 end
148 164
149 165 def month_name(month)
150 166 l(:actionview_datehelper_select_month_names).split(',')[month-1]
151 167 end
152 168
153 169 def syntax_highlight(name, content)
154 170 type = CodeRay::FileType[name]
155 171 type ? CodeRay.scan(content, type).html : h(content)
156 172 end
157 173
158 174 def to_path_param(path)
159 175 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
160 176 end
161 177
162 178 def pagination_links_full(paginator, count=nil, options={})
163 179 page_param = options.delete(:page_param) || :page
164 180 url_param = params.dup
165 181 # don't reuse params if filters are present
166 182 url_param.clear if url_param.has_key?(:set_filter)
167 183
168 184 html = ''
169 185 html << link_to_remote(('&#171; ' + l(:label_previous)),
170 186 {:update => 'content',
171 187 :url => url_param.merge(page_param => paginator.current.previous),
172 188 :complete => 'window.scrollTo(0,0)'},
173 189 {:href => url_for(:params => url_param.merge(page_param => paginator.current.previous))}) + ' ' if paginator.current.previous
174 190
175 191 html << (pagination_links_each(paginator, options) do |n|
176 192 link_to_remote(n.to_s,
177 193 {:url => {:params => url_param.merge(page_param => n)},
178 194 :update => 'content',
179 195 :complete => 'window.scrollTo(0,0)'},
180 196 {:href => url_for(:params => url_param.merge(page_param => n))})
181 197 end || '')
182 198
183 199 html << ' ' + link_to_remote((l(:label_next) + ' &#187;'),
184 200 {:update => 'content',
185 201 :url => url_param.merge(page_param => paginator.current.next),
186 202 :complete => 'window.scrollTo(0,0)'},
187 203 {:href => url_for(:params => url_param.merge(page_param => paginator.current.next))}) if paginator.current.next
188 204
189 205 unless count.nil?
190 206 html << [" (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})", per_page_links(paginator.items_per_page)].compact.join(' | ')
191 207 end
192 208
193 209 html
194 210 end
195 211
196 212 def per_page_links(selected=nil)
197 213 url_param = params.dup
198 214 url_param.clear if url_param.has_key?(:set_filter)
199 215
200 216 links = Setting.per_page_options_array.collect do |n|
201 217 n == selected ? n : link_to_remote(n, {:update => "content", :url => params.dup.merge(:per_page => n)},
202 218 {:href => url_for(url_param.merge(:per_page => n))})
203 219 end
204 220 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
205 221 end
206 222
207 223 def breadcrumb(*args)
208 224 elements = args.flatten
209 225 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
210 226 end
211 227
212 228 def html_title(*args)
213 229 if args.empty?
214 230 title = []
215 231 title << @project.name if @project
216 232 title += @html_title if @html_title
217 233 title << Setting.app_title
218 234 title.compact.join(' - ')
219 235 else
220 236 @html_title ||= []
221 237 @html_title += args
222 238 end
223 239 end
224 240
225 241 def accesskey(s)
226 242 Redmine::AccessKeys.key_for s
227 243 end
228 244
229 245 # Formats text according to system settings.
230 246 # 2 ways to call this method:
231 247 # * with a String: textilizable(text, options)
232 248 # * with an object and one of its attribute: textilizable(issue, :description, options)
233 249 def textilizable(*args)
234 250 options = args.last.is_a?(Hash) ? args.pop : {}
235 251 case args.size
236 252 when 1
237 253 obj = options[:object]
238 254 text = args.shift
239 255 when 2
240 256 obj = args.shift
241 257 text = obj.send(args.shift).to_s
242 258 else
243 259 raise ArgumentError, 'invalid arguments to textilizable'
244 260 end
245 261 return '' if text.blank?
246 262
247 263 only_path = options.delete(:only_path) == false ? false : true
248 264
249 265 # when using an image link, try to use an attachment, if possible
250 266 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
251 267
252 268 if attachments
253 269 attachments = attachments.sort_by(&:created_on).reverse
254 270 text = text.gsub(/!((\<|\=|\>)?(\([^\)]+\))?(\[[^\]]+\])?(\{[^\}]+\})?)(\S+\.(bmp|gif|jpg|jpeg|png))!/i) do |m|
255 271 style = $1
256 272 filename = $6
257 273 rf = Regexp.new(Regexp.escape(filename), Regexp::IGNORECASE)
258 274 # search for the picture in attachments
259 275 if found = attachments.detect { |att| att.filename =~ rf }
260 276 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
261 277 desc = found.description.to_s.gsub(/^([^\(\)]*).*$/, "\\1")
262 278 alt = desc.blank? ? nil : "(#{desc})"
263 279 "!#{style}#{image_url}#{alt}!"
264 280 else
265 281 "!#{style}#{filename}!"
266 282 end
267 283 end
268 284 end
269 285
270 286 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text) { |macro, args| exec_macro(macro, obj, args) }
271 287
272 288 # different methods for formatting wiki links
273 289 case options[:wiki_links]
274 290 when :local
275 291 # used for local links to html files
276 292 format_wiki_link = Proc.new {|project, title, anchor| "#{title}.html" }
277 293 when :anchor
278 294 # used for single-file wiki export
279 295 format_wiki_link = Proc.new {|project, title, anchor| "##{title}" }
280 296 else
281 297 format_wiki_link = Proc.new {|project, title, anchor| url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => project, :page => title, :anchor => anchor) }
282 298 end
283 299
284 300 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
285 301
286 302 # Wiki links
287 303 #
288 304 # Examples:
289 305 # [[mypage]]
290 306 # [[mypage|mytext]]
291 307 # wiki links can refer other project wikis, using project name or identifier:
292 308 # [[project:]] -> wiki starting page
293 309 # [[project:|mytext]]
294 310 # [[project:mypage]]
295 311 # [[project:mypage|mytext]]
296 312 text = text.gsub(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
297 313 link_project = project
298 314 esc, all, page, title = $1, $2, $3, $5
299 315 if esc.nil?
300 316 if page =~ /^([^\:]+)\:(.*)$/
301 317 link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
302 318 page = $2
303 319 title ||= $1 if page.blank?
304 320 end
305 321
306 322 if link_project && link_project.wiki
307 323 # extract anchor
308 324 anchor = nil
309 325 if page =~ /^(.+?)\#(.+)$/
310 326 page, anchor = $1, $2
311 327 end
312 328 # check if page exists
313 329 wiki_page = link_project.wiki.find_page(page)
314 330 link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page), anchor),
315 331 :class => ('wiki-page' + (wiki_page ? '' : ' new')))
316 332 else
317 333 # project or wiki doesn't exist
318 334 title || page
319 335 end
320 336 else
321 337 all
322 338 end
323 339 end
324 340
325 341 # Redmine links
326 342 #
327 343 # Examples:
328 344 # Issues:
329 345 # #52 -> Link to issue #52
330 346 # Changesets:
331 347 # r52 -> Link to revision 52
332 348 # commit:a85130f -> Link to scmid starting with a85130f
333 349 # Documents:
334 350 # document#17 -> Link to document with id 17
335 351 # document:Greetings -> Link to the document with title "Greetings"
336 352 # document:"Some document" -> Link to the document with title "Some document"
337 353 # Versions:
338 354 # version#3 -> Link to version with id 3
339 355 # version:1.0.0 -> Link to version named "1.0.0"
340 356 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
341 357 # Attachments:
342 358 # attachment:file.zip -> Link to the attachment of the current object named file.zip
343 359 # Source files:
344 360 # source:some/file -> Link to the file located at /some/file in the project's repository
345 361 # source:some/file@52 -> Link to the file's revision 52
346 362 # source:some/file#L120 -> Link to line 120 of the file
347 363 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
348 364 # export:some/file -> Force the download of the file
349 365 # Forum messages:
350 366 # message#1218 -> Link to message with id 1218
351 367 text = text.gsub(%r{([\s\(,\-\>]|^)(!)?(attachment|document|version|commit|source|export|message)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|\s|<|$)}) do |m|
352 368 leading, esc, prefix, sep, oid = $1, $2, $3, $5 || $7, $6 || $8
353 369 link = nil
354 370 if esc.nil?
355 371 if prefix.nil? && sep == 'r'
356 372 if project && (changeset = project.changesets.find_by_revision(oid))
357 373 link = link_to("r#{oid}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => oid},
358 374 :class => 'changeset',
359 375 :title => truncate_single_line(changeset.comments, 100))
360 376 end
361 377 elsif sep == '#'
362 378 oid = oid.to_i
363 379 case prefix
364 380 when nil
365 381 if issue = Issue.find_by_id(oid, :include => [:project, :status], :conditions => Project.visible_by(User.current))
366 382 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
367 383 :class => (issue.closed? ? 'issue closed' : 'issue'),
368 384 :title => "#{truncate(issue.subject, 100)} (#{issue.status.name})")
369 385 link = content_tag('del', link) if issue.closed?
370 386 end
371 387 when 'document'
372 388 if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
373 389 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
374 390 :class => 'document'
375 391 end
376 392 when 'version'
377 393 if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
378 394 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
379 395 :class => 'version'
380 396 end
381 397 when 'message'
382 398 if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
383 399 link = link_to h(truncate(message.subject, 60)), {:only_path => only_path,
384 400 :controller => 'messages',
385 401 :action => 'show',
386 402 :board_id => message.board,
387 403 :id => message.root,
388 404 :anchor => (message.parent ? "message-#{message.id}" : nil)},
389 405 :class => 'message'
390 406 end
391 407 end
392 408 elsif sep == ':'
393 409 # removes the double quotes if any
394 410 name = oid.gsub(%r{^"(.*)"$}, "\\1")
395 411 case prefix
396 412 when 'document'
397 413 if project && document = project.documents.find_by_title(name)
398 414 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
399 415 :class => 'document'
400 416 end
401 417 when 'version'
402 418 if project && version = project.versions.find_by_name(name)
403 419 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
404 420 :class => 'version'
405 421 end
406 422 when 'commit'
407 423 if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
408 424 link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
409 425 :class => 'changeset',
410 426 :title => truncate_single_line(changeset.comments, 100)
411 427 end
412 428 when 'source', 'export'
413 429 if project && project.repository
414 430 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
415 431 path, rev, anchor = $1, $3, $5
416 432 link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
417 433 :path => to_path_param(path),
418 434 :rev => rev,
419 435 :anchor => anchor,
420 436 :format => (prefix == 'export' ? 'raw' : nil)},
421 437 :class => (prefix == 'export' ? 'source download' : 'source')
422 438 end
423 439 when 'attachment'
424 440 if attachments && attachment = attachments.detect {|a| a.filename == name }
425 441 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
426 442 :class => 'attachment'
427 443 end
428 444 end
429 445 end
430 446 end
431 447 leading + (link || "#{prefix}#{sep}#{oid}")
432 448 end
433 449
434 450 text
435 451 end
436 452
437 453 # Same as Rails' simple_format helper without using paragraphs
438 454 def simple_format_without_paragraph(text)
439 455 text.to_s.
440 456 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
441 457 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
442 458 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
443 459 end
444 460
445 461 def error_messages_for(object_name, options = {})
446 462 options = options.symbolize_keys
447 463 object = instance_variable_get("@#{object_name}")
448 464 if object && !object.errors.empty?
449 465 # build full_messages here with controller current language
450 466 full_messages = []
451 467 object.errors.each do |attr, msg|
452 468 next if msg.nil?
453 469 msg = msg.first if msg.is_a? Array
454 470 if attr == "base"
455 471 full_messages << l(msg)
456 472 else
457 473 full_messages << "&#171; " + (l_has_string?("field_" + attr) ? l("field_" + attr) : object.class.human_attribute_name(attr)) + " &#187; " + l(msg) unless attr == "custom_values"
458 474 end
459 475 end
460 476 # retrieve custom values error messages
461 477 if object.errors[:custom_values]
462 478 object.custom_values.each do |v|
463 479 v.errors.each do |attr, msg|
464 480 next if msg.nil?
465 481 msg = msg.first if msg.is_a? Array
466 482 full_messages << "&#171; " + v.custom_field.name + " &#187; " + l(msg)
467 483 end
468 484 end
469 485 end
470 486 content_tag("div",
471 487 content_tag(
472 488 options[:header_tag] || "span", lwr(:gui_validation_error, full_messages.length) + ":"
473 489 ) +
474 490 content_tag("ul", full_messages.collect { |msg| content_tag("li", msg) }),
475 491 "id" => options[:id] || "errorExplanation", "class" => options[:class] || "errorExplanation"
476 492 )
477 493 else
478 494 ""
479 495 end
480 496 end
481 497
482 498 def lang_options_for_select(blank=true)
483 499 (blank ? [["(auto)", ""]] : []) +
484 500 GLoc.valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
485 501 end
486 502
487 503 def label_tag_for(name, option_tags = nil, options = {})
488 504 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
489 505 content_tag("label", label_text)
490 506 end
491 507
492 508 def labelled_tabular_form_for(name, object, options, &proc)
493 509 options[:html] ||= {}
494 510 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
495 511 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
496 512 end
497 513
498 514 def back_url_hidden_field_tag
499 515 back_url = params[:back_url] || request.env['HTTP_REFERER']
500 516 hidden_field_tag('back_url', back_url) unless back_url.blank?
501 517 end
502 518
503 519 def check_all_links(form_name)
504 520 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
505 521 " | " +
506 522 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
507 523 end
508 524
509 525 def progress_bar(pcts, options={})
510 526 pcts = [pcts, pcts] unless pcts.is_a?(Array)
511 527 pcts[1] = pcts[1] - pcts[0]
512 528 pcts << (100 - pcts[1] - pcts[0])
513 529 width = options[:width] || '100px;'
514 530 legend = options[:legend] || ''
515 531 content_tag('table',
516 532 content_tag('tr',
517 533 (pcts[0] > 0 ? content_tag('td', '', :width => "#{pcts[0].floor}%;", :class => 'closed') : '') +
518 534 (pcts[1] > 0 ? content_tag('td', '', :width => "#{pcts[1].floor}%;", :class => 'done') : '') +
519 535 (pcts[2] > 0 ? content_tag('td', '', :width => "#{pcts[2].floor}%;", :class => 'todo') : '')
520 536 ), :class => 'progress', :style => "width: #{width};") +
521 537 content_tag('p', legend, :class => 'pourcent')
522 538 end
523 539
524 540 def context_menu_link(name, url, options={})
525 541 options[:class] ||= ''
526 542 if options.delete(:selected)
527 543 options[:class] << ' icon-checked disabled'
528 544 options[:disabled] = true
529 545 end
530 546 if options.delete(:disabled)
531 547 options.delete(:method)
532 548 options.delete(:confirm)
533 549 options.delete(:onclick)
534 550 options[:class] << ' disabled'
535 551 url = '#'
536 552 end
537 553 link_to name, url, options
538 554 end
539 555
540 556 def calendar_for(field_id)
541 557 include_calendar_headers_tags
542 558 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
543 559 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
544 560 end
545 561
546 562 def include_calendar_headers_tags
547 563 unless @calendar_headers_tags_included
548 564 @calendar_headers_tags_included = true
549 565 content_for :header_tags do
550 566 javascript_include_tag('calendar/calendar') +
551 567 javascript_include_tag("calendar/lang/calendar-#{current_language}.js") +
552 568 javascript_include_tag('calendar/calendar-setup') +
553 569 stylesheet_link_tag('calendar')
554 570 end
555 571 end
556 572 end
557 573
558 574 def content_for(name, content = nil, &block)
559 575 @has_content ||= {}
560 576 @has_content[name] = true
561 577 super(name, content, &block)
562 578 end
563 579
564 580 def has_content?(name)
565 581 (@has_content && @has_content[name]) || false
566 582 end
567 583
568 584 # Returns the avatar image tag for the given +user+ if avatars are enabled
569 585 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
570 586 def avatar(user, options = { })
571 587 if Setting.gravatar_enabled?
572 588 email = nil
573 589 if user.respond_to?(:mail)
574 590 email = user.mail
575 591 elsif user.to_s =~ %r{<(.+?)>}
576 592 email = $1
577 593 end
578 594 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
579 595 end
580 596 end
581 597
582 598 private
583 599
584 600 def wiki_helper
585 601 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
586 602 extend helper
587 603 return self
588 604 end
589 605 end
@@ -1,72 +1,56
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 module WikiHelper
19
20 def render_page_hierarchy(pages, node=nil)
21 content = ''
22 if pages[node]
23 content << "<ul class=\"pages-hierarchy\">\n"
24 pages[node].each do |page|
25 content << "<li>"
26 content << link_to(h(page.pretty_title), {:action => 'index', :page => page.title},
27 :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
28 content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
29 content << "</li>\n"
30 end
31 content << "</ul>\n"
32 end
33 content
34 end
35 19
36 20 def html_diff(wdiff)
37 21 words = wdiff.words.collect{|word| h(word)}
38 22 words_add = 0
39 23 words_del = 0
40 24 dels = 0
41 25 del_off = 0
42 26 wdiff.diff.diffs.each do |diff|
43 27 add_at = nil
44 28 add_to = nil
45 29 del_at = nil
46 30 deleted = ""
47 31 diff.each do |change|
48 32 pos = change[1]
49 33 if change[0] == "+"
50 34 add_at = pos + dels unless add_at
51 35 add_to = pos + dels
52 36 words_add += 1
53 37 else
54 38 del_at = pos unless del_at
55 39 deleted << ' ' + change[2]
56 40 words_del += 1
57 41 end
58 42 end
59 43 if add_at
60 44 words[add_at] = '<span class="diff_in">' + words[add_at]
61 45 words[add_to] = words[add_to] + '</span>'
62 46 end
63 47 if del_at
64 48 words.insert del_at - del_off + dels + words_add, '<span class="diff_out">' + deleted + '</span>'
65 49 dels += 1
66 50 del_off += words_del
67 51 words_del = 0
68 52 end
69 53 end
70 54 simple_format_without_paragraph(words.join(' '))
71 55 end
72 56 end
@@ -1,54 +1,73
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Wiki < ActiveRecord::Base
19 19 belongs_to :project
20 20 has_many :pages, :class_name => 'WikiPage', :dependent => :destroy, :order => 'title'
21 21 has_many :redirects, :class_name => 'WikiRedirect', :dependent => :delete_all
22 22
23 23 validates_presence_of :start_page
24 24 validates_format_of :start_page, :with => /^[^,\.\/\?\;\|\:]*$/
25 25
26 26 # find the page with the given title
27 27 # if page doesn't exist, return a new page
28 28 def find_or_new_page(title)
29 29 title = start_page if title.blank?
30 30 find_page(title) || WikiPage.new(:wiki => self, :title => Wiki.titleize(title))
31 31 end
32 32
33 33 # find the page with the given title
34 34 def find_page(title, options = {})
35 35 title = start_page if title.blank?
36 36 title = Wiki.titleize(title)
37 37 page = pages.find_by_title(title)
38 38 if !page && !(options[:with_redirect] == false)
39 39 # search for a redirect
40 40 redirect = redirects.find_by_title(title)
41 41 page = find_page(redirect.redirects_to, :with_redirect => false) if redirect
42 42 end
43 43 page
44 44 end
45 45
46 # Finds a page by title
47 # The given string can be of one of the forms: "title" or "project:title"
48 # Examples:
49 # Wiki.find_page("bar", project => foo)
50 # Wiki.find_page("foo:bar")
51 def self.find_page(title, options = {})
52 project = options[:project]
53 if title.to_s =~ %r{^([^\:]+)\:(.*)$}
54 project_identifier, title = $1, $2
55 project = Project.find_by_identifier(project_identifier) || Project.find_by_name(project_identifier)
56 end
57 if project && project.wiki
58 page = project.wiki.find_page(title)
59 if page && page.content
60 page
61 end
62 end
63 end
64
46 65 # turn a string into a valid page title
47 66 def self.titleize(title)
48 67 # replace spaces with _ and remove unwanted caracters
49 68 title = title.gsub(/\s+/, '_').delete(',./?;|:') if title
50 69 # upcase the first letter
51 70 title = (title.slice(0..0).upcase + (title.slice(1..-1) || '')) if title
52 71 title
53 72 end
54 73 end
@@ -1,107 +1,121
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 module Redmine
19 19 module WikiFormatting
20 20 module Macros
21 21 module Definitions
22 22 def exec_macro(name, obj, args)
23 23 method_name = "macro_#{name}"
24 24 send(method_name, obj, args) if respond_to?(method_name)
25 25 end
26
27 def extract_macro_options(args, *keys)
28 options = {}
29 while args.last.to_s.strip =~ %r{^(.+)\=(.+)$} && keys.include?($1.downcase.to_sym)
30 options[$1.downcase.to_sym] = $2
31 args.pop
32 end
33 return [args, options]
34 end
26 35 end
27 36
28 37 @@available_macros = {}
29 38
30 39 class << self
31 40 # Called with a block to define additional macros.
32 41 # Macro blocks accept 2 arguments:
33 42 # * obj: the object that is rendered
34 43 # * args: macro arguments
35 44 #
36 45 # Plugins can use this method to define new macros:
37 46 #
38 47 # Redmine::WikiFormatting::Macros.register do
39 48 # desc "This is my macro"
40 49 # macro :my_macro do |obj, args|
41 50 # "My macro output"
42 51 # end
43 52 # end
44 53 def register(&block)
45 54 class_eval(&block) if block_given?
46 55 end
47 56
48 57 private
49 58 # Defines a new macro with the given name and block.
50 59 def macro(name, &block)
51 60 name = name.to_sym if name.is_a?(String)
52 61 @@available_macros[name] = @@desc || ''
53 62 @@desc = nil
54 63 raise "Can not create a macro without a block!" unless block_given?
55 64 Definitions.send :define_method, "macro_#{name}".downcase, &block
56 65 end
57 66
58 67 # Sets description for the next macro to be defined
59 68 def desc(txt)
60 69 @@desc = txt
61 70 end
62 71 end
63 72
64 73 # Builtin macros
65 74 desc "Sample macro."
66 75 macro :hello_world do |obj, args|
67 76 "Hello world! Object: #{obj.class.name}, " + (args.empty? ? "Called with no argument." : "Arguments: #{args.join(', ')}")
68 77 end
69 78
70 79 desc "Displays a list of all available macros, including description if available."
71 80 macro :macro_list do
72 81 out = ''
73 82 @@available_macros.keys.collect(&:to_s).sort.each do |macro|
74 83 out << content_tag('dt', content_tag('code', macro))
75 84 out << content_tag('dd', textilizable(@@available_macros[macro.to_sym]))
76 85 end
77 86 content_tag('dl', out)
78 87 end
79 88
80 desc "Displays a list of child pages."
89 desc "Displays a list of child pages. With no argument, it displays the child pages of the current wiki page. Examples:\n\n" +
90 " !{{child_pages}} -- can be used from a wiki page only\n" +
91 " !{{child_pages(Foo)}} -- lists all children of page Foo\n" +
92 " !{{child_pages(Foo, parent=1)}} -- same as above with a link to page Foo"
81 93 macro :child_pages do |obj, args|
82 raise 'This macro applies to wiki pages only.' unless obj.is_a?(WikiContent)
83 render_page_hierarchy(obj.page.descendants.group_by(&:parent_id), obj.page.id)
94 args, options = extract_macro_options(args, :parent)
95 page = nil
96 if args.size > 0
97 page = Wiki.find_page(args.first.to_s, :project => @project)
98 elsif obj.is_a?(WikiContent)
99 page = obj.page
100 else
101 raise 'With no argument, this macro can be called from wiki pages only.'
102 end
103 raise 'Page not found' if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project)
104 pages = ([page] + page.descendants).group_by(&:parent_id)
105 render_page_hierarchy(pages, options[:parent] ? page.parent_id : page.id)
84 106 end
85 107
86 108 desc "Include a wiki page. Example:\n\n !{{include(Foo)}}\n\nor to include a page of a specific project wiki:\n\n !{{include(projectname:Foo)}}"
87 109 macro :include do |obj, args|
88 project = @project
89 title = args.first.to_s
90 if title =~ %r{^([^\:]+)\:(.*)$}
91 project_identifier, title = $1, $2
92 project = Project.find_by_identifier(project_identifier) || Project.find_by_name(project_identifier)
93 end
94 raise 'Unknow project' unless project && User.current.allowed_to?(:view_wiki_pages, project)
95 raise 'No wiki for this project' unless !project.wiki.nil?
96 page = project.wiki.find_page(title)
97 raise "Page #{args.first} doesn't exist" unless page && page.content
110 page = Wiki.find_page(args.first.to_s, :project => @project)
111 raise 'Page not found' if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project)
98 112 @included_wiki_pages ||= []
99 113 raise 'Circular inclusion detected' if @included_wiki_pages.include?(page.title)
100 114 @included_wiki_pages << page.title
101 115 out = textilizable(page.content, :text, :attachments => page.attachments)
102 116 @included_wiki_pages.pop
103 117 out
104 118 end
105 119 end
106 120 end
107 121 end
@@ -1,50 +1,72
1 1 ---
2 2 wiki_contents_001:
3 3 text: |-
4 4 h1. CookBook documentation
5 5
6 6 {{child_pages}}
7 7
8 8 Some updated [[documentation]] here with gzipped history
9 9 updated_on: 2007-03-07 00:10:51 +01:00
10 10 page_id: 1
11 11 id: 1
12 12 version: 3
13 13 author_id: 1
14 14 comments: Gzip compression activated
15 15 wiki_contents_002:
16 16 text: |-
17 17 h1. Another page
18 18
19 19 This is a link to a ticket: #2
20 20 And this is an included page:
21 21 {{include(Page with an inline image)}}
22 22 updated_on: 2007-03-08 00:18:07 +01:00
23 23 page_id: 2
24 24 id: 2
25 25 version: 1
26 26 author_id: 1
27 27 comments:
28 28 wiki_contents_003:
29 29 text: |-
30 30 h1. Start page
31 31
32 32 E-commerce web site start page
33 33 updated_on: 2007-03-08 00:18:07 +01:00
34 34 page_id: 3
35 35 id: 3
36 36 version: 1
37 37 author_id: 1
38 38 comments:
39 39 wiki_contents_004:
40 40 text: |-
41 41 h1. Page with an inline image
42 42
43 43 This is an inline image:
44 44
45 45 !logo.gif!
46 46 updated_on: 2007-03-08 00:18:07 +01:00
47 47 page_id: 4
48 48 id: 4
49 49 version: 1
50 author_id: 1
51 comments:
52 wiki_contents_005:
53 text: |-
54 h1. Child page 1
55
56 This is a child page
57 updated_on: 2007-03-08 00:18:07 +01:00
58 page_id: 5
59 id: 5
60 version: 1
61 author_id: 1
62 comments:
63 wiki_contents_006:
64 text: |-
65 h1. Child page 2
66
67 This is a child page
68 updated_on: 2007-03-08 00:18:07 +01:00
69 page_id: 6
70 id: 6
71 version: 1
50 72 author_id: 1 No newline at end of file
@@ -1,30 +1,44
1 1 ---
2 2 wiki_pages_001:
3 3 created_on: 2007-03-07 00:08:07 +01:00
4 4 title: CookBook_documentation
5 5 id: 1
6 6 wiki_id: 1
7 7 protected: true
8 8 parent_id:
9 9 wiki_pages_002:
10 10 created_on: 2007-03-08 00:18:07 +01:00
11 11 title: Another_page
12 12 id: 2
13 13 wiki_id: 1
14 14 protected: false
15 15 parent_id:
16 16 wiki_pages_003:
17 17 created_on: 2007-03-08 00:18:07 +01:00
18 18 title: Start_page
19 19 id: 3
20 20 wiki_id: 2
21 21 protected: false
22 22 parent_id:
23 23 wiki_pages_004:
24 24 created_on: 2007-03-08 00:18:07 +01:00
25 25 title: Page_with_an_inline_image
26 26 id: 4
27 27 wiki_id: 1
28 28 protected: false
29 29 parent_id: 1
30 wiki_pages_005:
31 created_on: 2007-03-08 00:18:07 +01:00
32 title: Child_1
33 id: 5
34 wiki_id: 1
35 protected: false
36 parent_id: 2
37 wiki_pages_006:
38 created_on: 2007-03-08 00:18:07 +01:00
39 title: Child_2
40 id: 6
41 wiki_id: 1
42 protected: false
43 parent_id: 2
30 44 No newline at end of file
@@ -1,457 +1,431
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../../test_helper'
19 19
20 20 class ApplicationHelperTest < HelperTestCase
21 21 include ApplicationHelper
22 22 include ActionView::Helpers::TextHelper
23 23 fixtures :projects, :roles, :enabled_modules, :users,
24 24 :repositories, :changesets,
25 25 :trackers, :issue_statuses, :issues, :versions, :documents,
26 26 :wikis, :wiki_pages, :wiki_contents,
27 27 :boards, :messages,
28 28 :attachments
29 29
30 30 def setup
31 31 super
32 32 end
33 33
34 34 def test_auto_links
35 35 to_test = {
36 36 'http://foo.bar' => '<a class="external" href="http://foo.bar">http://foo.bar</a>',
37 37 'http://foo.bar/~user' => '<a class="external" href="http://foo.bar/~user">http://foo.bar/~user</a>',
38 38 'http://foo.bar.' => '<a class="external" href="http://foo.bar">http://foo.bar</a>.',
39 39 'https://foo.bar.' => '<a class="external" href="https://foo.bar">https://foo.bar</a>.',
40 40 'This is a link: http://foo.bar.' => 'This is a link: <a class="external" href="http://foo.bar">http://foo.bar</a>.',
41 41 'A link (eg. http://foo.bar).' => 'A link (eg. <a class="external" href="http://foo.bar">http://foo.bar</a>).',
42 42 '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>.',
43 43 'http://www.foo.bar/Test_(foobar)' => '<a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>',
44 44 '(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>)',
45 45 '(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>)',
46 46 '(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>).',
47 47 '(see "inline link":http://www.foo.bar/Test_(foobar))' => '(see <a href="http://www.foo.bar/Test_(foobar)" class="external">inline link</a>)',
48 48 '(see "inline link":http://www.foo.bar/Test)' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>)',
49 49 '(see "inline link":http://www.foo.bar/Test).' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>).',
50 50 'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>',
51 51 '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>',
52 52 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>',
53 53 'http://foo@www.bar.com' => '<a class="external" href="http://foo@www.bar.com">http://foo@www.bar.com</a>',
54 54 'http://foo:bar@www.bar.com' => '<a class="external" href="http://foo:bar@www.bar.com">http://foo:bar@www.bar.com</a>',
55 55 'ftp://foo.bar' => '<a class="external" href="ftp://foo.bar">ftp://foo.bar</a>',
56 56 'ftps://foo.bar' => '<a class="external" href="ftps://foo.bar">ftps://foo.bar</a>',
57 57 'sftp://foo.bar' => '<a class="external" href="sftp://foo.bar">sftp://foo.bar</a>',
58 58 }
59 59 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
60 60 end
61 61
62 62 def test_auto_mailto
63 63 assert_equal '<p><a href="mailto:test@foo.bar" class="email">test@foo.bar</a></p>',
64 64 textilizable('test@foo.bar')
65 65 end
66 66
67 67 def test_inline_images
68 68 to_test = {
69 69 '!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
70 70 'floating !>http://foo.bar/image.jpg!' => 'floating <div style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></div>',
71 71 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class <img src="http://foo.bar/image.jpg" class="some-class" alt="" />',
72 72 'with style !{width:100px;height100px}http://foo.bar/image.jpg!' => 'with style <img src="http://foo.bar/image.jpg" style="width:100px;height100px;" alt="" />',
73 73 }
74 74 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
75 75 end
76 76
77 77 def test_attached_images
78 78 to_test = {
79 79 'Inline image: !logo.gif!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
80 80 'Inline image: !logo.GIF!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />'
81 81 }
82 82 attachments = Attachment.find(:all)
83 83 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
84 84 end
85 85
86 86 def test_textile_external_links
87 87 to_test = {
88 88 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>',
89 89 'This is an intern "link":/foo/bar' => 'This is an intern <a href="/foo/bar">link</a>',
90 90 '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>',
91 91 "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":</p>\n\n\n\t<p>Another paragraph",
92 92 # no multiline link text
93 93 "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 />\nand another on a second line\":test"
94 94 }
95 95 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
96 96 end
97 97
98 98 def test_redmine_links
99 99 issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3},
100 100 :class => 'issue', :title => 'Error 281 when updating a recipe (New)')
101 101
102 102 changeset_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
103 103 :class => 'changeset', :title => 'My very first commit')
104 104
105 105 document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
106 106 :class => 'document')
107 107
108 108 version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
109 109 :class => 'version')
110 110
111 111 message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4}
112 112
113 113 source_url = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}
114 114 source_url_with_ext = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file.ext']}
115 115
116 116 to_test = {
117 117 # tickets
118 118 '#3, #3 and #3.' => "#{issue_link}, #{issue_link} and #{issue_link}.",
119 119 # changesets
120 120 'r1' => changeset_link,
121 121 # documents
122 122 'document#1' => document_link,
123 123 'document:"Test document"' => document_link,
124 124 # versions
125 125 'version#2' => version_link,
126 126 'version:1.0' => version_link,
127 127 'version:"1.0"' => version_link,
128 128 # source
129 129 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
130 130 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".",
131 131 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
132 132 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".",
133 133 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
134 134 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",",
135 135 'source:/some/file@52' => link_to('source:/some/file@52', source_url.merge(:rev => 52), :class => 'source'),
136 136 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_ext.merge(:rev => 52), :class => 'source'),
137 137 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url.merge(:anchor => 'L110'), :class => 'source'),
138 138 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext.merge(:anchor => 'L110'), :class => 'source'),
139 139 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url.merge(:rev => 52, :anchor => 'L110'), :class => 'source'),
140 140 'export:/some/file' => link_to('export:/some/file', source_url.merge(:format => 'raw'), :class => 'source download'),
141 141 # message
142 142 'message#4' => link_to('Post 2', message_url, :class => 'message'),
143 143 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5'), :class => 'message'),
144 144 # escaping
145 145 '!#3.' => '#3.',
146 146 '!r1' => 'r1',
147 147 '!document#1' => 'document#1',
148 148 '!document:"Test document"' => 'document:"Test document"',
149 149 '!version#2' => 'version#2',
150 150 '!version:1.0' => 'version:1.0',
151 151 '!version:"1.0"' => 'version:"1.0"',
152 152 '!source:/some/file' => 'source:/some/file',
153 153 # invalid expressions
154 154 'source:' => 'source:',
155 155 # url hash
156 156 "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
157 157 }
158 158 @project = Project.find(1)
159 159 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
160 160 end
161 161
162 162 def test_wiki_links
163 163 to_test = {
164 164 '[[CookBook documentation]]' => '<a href="/wiki/ecookbook/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
165 165 '[[Another page|Page]]' => '<a href="/wiki/ecookbook/Another_page" class="wiki-page">Page</a>',
166 166 # link with anchor
167 167 '[[CookBook documentation#One-section]]' => '<a href="/wiki/ecookbook/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
168 168 '[[Another page#anchor|Page]]' => '<a href="/wiki/ecookbook/Another_page#anchor" class="wiki-page">Page</a>',
169 169 # page that doesn't exist
170 170 '[[Unknown page]]' => '<a href="/wiki/ecookbook/Unknown_page" class="wiki-page new">Unknown page</a>',
171 171 '[[Unknown page|404]]' => '<a href="/wiki/ecookbook/Unknown_page" class="wiki-page new">404</a>',
172 172 # link to another project wiki
173 173 '[[onlinestore:]]' => '<a href="/wiki/onlinestore/" class="wiki-page">onlinestore</a>',
174 174 '[[onlinestore:|Wiki]]' => '<a href="/wiki/onlinestore/" class="wiki-page">Wiki</a>',
175 175 '[[onlinestore:Start page]]' => '<a href="/wiki/onlinestore/Start_page" class="wiki-page">Start page</a>',
176 176 '[[onlinestore:Start page|Text]]' => '<a href="/wiki/onlinestore/Start_page" class="wiki-page">Text</a>',
177 177 '[[onlinestore:Unknown page]]' => '<a href="/wiki/onlinestore/Unknown_page" class="wiki-page new">Unknown page</a>',
178 178 # striked through link
179 179 '-[[Another page|Page]]-' => '<del><a href="/wiki/ecookbook/Another_page" class="wiki-page">Page</a></del>',
180 180 '-[[Another page|Page]] link-' => '<del><a href="/wiki/ecookbook/Another_page" class="wiki-page">Page</a> link</del>',
181 181 # escaping
182 182 '![[Another page|Page]]' => '[[Another page|Page]]',
183 183 }
184 184 @project = Project.find(1)
185 185 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
186 186 end
187 187
188 188 def test_html_tags
189 189 to_test = {
190 190 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
191 191 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
192 192 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
193 193 # do not escape pre/code tags
194 194 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
195 195 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
196 196 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
197 197 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
198 198 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
199 199 # remove attributes except class
200 200 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
201 201 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
202 202 }
203 203 to_test.each { |text, result| assert_equal result, textilizable(text) }
204 204 end
205 205
206 206 def test_allowed_html_tags
207 207 to_test = {
208 208 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
209 209 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
210 210 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
211 211 }
212 212 to_test.each { |text, result| assert_equal result, textilizable(text) }
213 213 end
214 214
215 215 def syntax_highlight
216 216 raw = <<-RAW
217 217 <pre><code class="ruby">
218 218 # Some ruby code here
219 219 </pre></code>
220 220 RAW
221 221
222 222 expected = <<-EXPECTED
223 223 <pre><code class="ruby CodeRay"><span class="no">1</span> <span class="c"># Some ruby code here</span>
224 224 </pre></code>
225 225 EXPECTED
226 226
227 227 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
228 228 end
229 229
230 230 def test_wiki_links_in_tables
231 231 to_test = {"|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" =>
232 232 '<tr><td><a href="/wiki/ecookbook/Page" class="wiki-page new">Link title</a></td>' +
233 233 '<td><a href="/wiki/ecookbook/Other_Page" class="wiki-page new">Other title</a></td>' +
234 234 '</tr><tr><td>Cell 21</td><td><a href="/wiki/ecookbook/Last_page" class="wiki-page new">Last page</a></td></tr>'
235 235 }
236 236 @project = Project.find(1)
237 237 to_test.each { |text, result| assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '') }
238 238 end
239 239
240 240 def test_text_formatting
241 241 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
242 242 '(_text within parentheses_)' => '(<em>text within parentheses</em>)'
243 243 }
244 244 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
245 245 end
246 246
247 247 def test_wiki_horizontal_rule
248 248 assert_equal '<hr />', textilizable('---')
249 249 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
250 250 end
251 251
252 252 def test_acronym
253 253 assert_equal '<p>This is an acronym: <acronym title="American Civil Liberties Union">ACLU</acronym>.</p>',
254 254 textilizable('This is an acronym: ACLU(American Civil Liberties Union).')
255 255 end
256 256
257 257 def test_footnotes
258 258 raw = <<-RAW
259 259 This is some text[1].
260 260
261 261 fn1. This is the foot note
262 262 RAW
263 263
264 264 expected = <<-EXPECTED
265 265 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
266 266 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
267 267 EXPECTED
268 268
269 269 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
270 270 end
271 271
272 272 def test_table_of_content
273 273 raw = <<-RAW
274 274 {{toc}}
275 275
276 276 h1. Title
277 277
278 278 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
279 279
280 280 h2. Subtitle
281 281
282 282 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
283 283
284 284 h2. Subtitle with %{color:red}red text%
285 285
286 286 h1. Another title
287 287
288 288 RAW
289 289
290 290 expected = '<ul class="toc">' +
291 291 '<li class="heading1"><a href="#Title">Title</a></li>' +
292 292 '<li class="heading2"><a href="#Subtitle">Subtitle</a></li>' +
293 293 '<li class="heading2"><a href="#Subtitle-with-red-text">Subtitle with red text</a></li>' +
294 294 '<li class="heading1"><a href="#Another-title">Another title</a></li>' +
295 295 '</ul>'
296 296
297 297 assert textilizable(raw).gsub("\n", "").include?(expected)
298 298 end
299 299
300 300 def test_blockquote
301 301 # orig raw text
302 302 raw = <<-RAW
303 303 John said:
304 304 > Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
305 305 > Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
306 306 > * Donec odio lorem,
307 307 > * sagittis ac,
308 308 > * malesuada in,
309 309 > * adipiscing eu, dolor.
310 310 >
311 311 > >Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.
312 312 > Proin a tellus. Nam vel neque.
313 313
314 314 He's right.
315 315 RAW
316 316
317 317 # expected html
318 318 expected = <<-EXPECTED
319 319 <p>John said:</p>
320 320 <blockquote>
321 321 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
322 322 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
323 323 <ul>
324 324 <li>Donec odio lorem,</li>
325 325 <li>sagittis ac,</li>
326 326 <li>malesuada in,</li>
327 327 <li>adipiscing eu, dolor.</li>
328 328 </ul>
329 329 <blockquote>
330 330 <p>Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.</p>
331 331 </blockquote>
332 332 <p>Proin a tellus. Nam vel neque.</p>
333 333 </blockquote>
334 334 <p>He's right.</p>
335 335 EXPECTED
336 336
337 337 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
338 338 end
339 339
340 340 def test_table
341 341 raw = <<-RAW
342 342 This is a table with empty cells:
343 343
344 344 |cell11|cell12||
345 345 |cell21||cell23|
346 346 |cell31|cell32|cell33|
347 347 RAW
348 348
349 349 expected = <<-EXPECTED
350 350 <p>This is a table with empty cells:</p>
351 351
352 352 <table>
353 353 <tr><td>cell11</td><td>cell12</td><td></td></tr>
354 354 <tr><td>cell21</td><td></td><td>cell23</td></tr>
355 355 <tr><td>cell31</td><td>cell32</td><td>cell33</td></tr>
356 356 </table>
357 357 EXPECTED
358 358
359 359 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
360 360 end
361 361
362 def test_macro_hello_world
363 text = "{{hello_world}}"
364 assert textilizable(text).match(/Hello world!/)
365 # escaping
366 text = "!{{hello_world}}"
367 assert_equal '<p>{{hello_world}}</p>', textilizable(text)
368 end
369
370 def test_macro_include
371 @project = Project.find(1)
372 # include a page of the current project wiki
373 text = "{{include(Another page)}}"
374 assert textilizable(text).match(/This is a link to a ticket/)
375
376 @project = nil
377 # include a page of a specific project wiki
378 text = "{{include(ecookbook:Another page)}}"
379 assert textilizable(text).match(/This is a link to a ticket/)
380
381 text = "{{include(ecookbook:)}}"
382 assert textilizable(text).match(/CookBook documentation/)
383
384 text = "{{include(unknowidentifier:somepage)}}"
385 assert textilizable(text).match(/Unknow project/)
386 end
387
388 362 def test_default_formatter
389 363 Setting.text_formatting = 'unknown'
390 364 text = 'a *link*: http://www.example.net/'
391 365 assert_equal '<p>a *link*: <a href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
392 366 Setting.text_formatting = 'textile'
393 367 end
394 368
395 369 def test_date_format_default
396 370 today = Date.today
397 371 Setting.date_format = ''
398 372 assert_equal l_date(today), format_date(today)
399 373 end
400 374
401 375 def test_date_format
402 376 today = Date.today
403 377 Setting.date_format = '%d %m %Y'
404 378 assert_equal today.strftime('%d %m %Y'), format_date(today)
405 379 end
406 380
407 381 def test_time_format_default
408 382 now = Time.now
409 383 Setting.date_format = ''
410 384 Setting.time_format = ''
411 385 assert_equal l_datetime(now), format_time(now)
412 386 assert_equal l_time(now), format_time(now, false)
413 387 end
414 388
415 389 def test_time_format
416 390 now = Time.now
417 391 Setting.date_format = '%d %m %Y'
418 392 Setting.time_format = '%H %M'
419 393 assert_equal now.strftime('%d %m %Y %H %M'), format_time(now)
420 394 assert_equal now.strftime('%H %M'), format_time(now, false)
421 395 end
422 396
423 397 def test_utc_time_format
424 398 now = Time.now.utc
425 399 Setting.date_format = '%d %m %Y'
426 400 Setting.time_format = '%H %M'
427 401 assert_equal Time.now.strftime('%d %m %Y %H %M'), format_time(now)
428 402 assert_equal Time.now.strftime('%H %M'), format_time(now, false)
429 403 end
430 404
431 405 def test_due_date_distance_in_words
432 406 to_test = { Date.today => 'Due in 0 days',
433 407 Date.today + 1 => 'Due in 1 day',
434 408 Date.today + 100 => 'Due in 100 days',
435 409 Date.today + 20000 => 'Due in 20000 days',
436 410 Date.today - 1 => '1 day late',
437 411 Date.today - 100 => '100 days late',
438 412 Date.today - 20000 => '20000 days late',
439 413 }
440 414 to_test.each do |date, expected|
441 415 assert_equal expected, due_date_distance_in_words(date)
442 416 end
443 417 end
444 418
445 419 def test_avatar
446 420 # turn on avatars
447 421 Setting.gravatar_enabled = '1'
448 422 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
449 423 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
450 424 assert_nil avatar('jsmith')
451 425 assert_nil avatar(nil)
452 426
453 427 # turn off avatars
454 428 Setting.gravatar_enabled = '0'
455 429 assert_nil avatar(User.find_by_mail('jsmith@somenet.foo'))
456 430 end
457 431 end
General Comments 0
You need to be logged in to leave comments. Login now