##// END OF EJS Templates
Fixes section edit links when text includes pre/code tag (#2222)....
Jean-Philippe Lang -
r7715:b3b2eb3e50c5
parent child
Show More
@@ -1,1031 +1,1031
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2011 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 require 'forwardable'
21 21 require 'cgi'
22 22
23 23 module ApplicationHelper
24 24 include Redmine::WikiFormatting::Macros::Definitions
25 25 include Redmine::I18n
26 26 include GravatarHelper::PublicMethods
27 27
28 28 extend Forwardable
29 29 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
30 30
31 31 # Return true if user is authorized for controller/action, otherwise false
32 32 def authorize_for(controller, action)
33 33 User.current.allowed_to?({:controller => controller, :action => action}, @project)
34 34 end
35 35
36 36 # Display a link if user is authorized
37 37 #
38 38 # @param [String] name Anchor text (passed to link_to)
39 39 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
40 40 # @param [optional, Hash] html_options Options passed to link_to
41 41 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
42 42 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
43 43 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
44 44 end
45 45
46 46 # Display a link to remote if user is authorized
47 47 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
48 48 url = options[:url] || {}
49 49 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
50 50 end
51 51
52 52 # Displays a link to user's account page if active
53 53 def link_to_user(user, options={})
54 54 if user.is_a?(User)
55 55 name = h(user.name(options[:format]))
56 56 if user.active?
57 57 link_to name, :controller => 'users', :action => 'show', :id => user
58 58 else
59 59 name
60 60 end
61 61 else
62 62 h(user.to_s)
63 63 end
64 64 end
65 65
66 66 # Displays a link to +issue+ with its subject.
67 67 # Examples:
68 68 #
69 69 # link_to_issue(issue) # => Defect #6: This is the subject
70 70 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
71 71 # link_to_issue(issue, :subject => false) # => Defect #6
72 72 # link_to_issue(issue, :project => true) # => Foo - Defect #6
73 73 #
74 74 def link_to_issue(issue, options={})
75 75 title = nil
76 76 subject = nil
77 77 if options[:subject] == false
78 78 title = truncate(issue.subject, :length => 60)
79 79 else
80 80 subject = issue.subject
81 81 if options[:truncate]
82 82 subject = truncate(subject, :length => options[:truncate])
83 83 end
84 84 end
85 85 s = link_to "#{h(issue.tracker)} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
86 86 :class => issue.css_classes,
87 87 :title => title
88 88 s << ": #{h subject}" if subject
89 89 s = "#{h issue.project} - " + s if options[:project]
90 90 s
91 91 end
92 92
93 93 # Generates a link to an attachment.
94 94 # Options:
95 95 # * :text - Link text (default to attachment filename)
96 96 # * :download - Force download (default: false)
97 97 def link_to_attachment(attachment, options={})
98 98 text = options.delete(:text) || attachment.filename
99 99 action = options.delete(:download) ? 'download' : 'show'
100 100
101 101 link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
102 102 end
103 103
104 104 # Generates a link to a SCM revision
105 105 # Options:
106 106 # * :text - Link text (default to the formatted revision)
107 107 def link_to_revision(revision, project, options={})
108 108 text = options.delete(:text) || format_revision(revision)
109 109 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
110 110
111 111 link_to(h(text), {:controller => 'repositories', :action => 'revision', :id => project, :rev => rev},
112 112 :title => l(:label_revision_id, format_revision(revision)))
113 113 end
114 114
115 115 # Generates a link to a message
116 116 def link_to_message(message, options={}, html_options = nil)
117 117 link_to(
118 118 h(truncate(message.subject, :length => 60)),
119 119 { :controller => 'messages', :action => 'show',
120 120 :board_id => message.board_id,
121 121 :id => message.root,
122 122 :r => (message.parent_id && message.id),
123 123 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
124 124 }.merge(options),
125 125 html_options
126 126 )
127 127 end
128 128
129 129 # Generates a link to a project if active
130 130 # Examples:
131 131 #
132 132 # link_to_project(project) # => link to the specified project overview
133 133 # link_to_project(project, :action=>'settings') # => link to project settings
134 134 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
135 135 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
136 136 #
137 137 def link_to_project(project, options={}, html_options = nil)
138 138 if project.active?
139 139 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
140 140 link_to(h(project), url, html_options)
141 141 else
142 142 h(project)
143 143 end
144 144 end
145 145
146 146 def toggle_link(name, id, options={})
147 147 onclick = "Element.toggle('#{id}'); "
148 148 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
149 149 onclick << "return false;"
150 150 link_to(name, "#", :onclick => onclick)
151 151 end
152 152
153 153 def image_to_function(name, function, html_options = {})
154 154 html_options.symbolize_keys!
155 155 tag(:input, html_options.merge({
156 156 :type => "image", :src => image_path(name),
157 157 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
158 158 }))
159 159 end
160 160
161 161 def prompt_to_remote(name, text, param, url, html_options = {})
162 162 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
163 163 link_to name, {}, html_options
164 164 end
165 165
166 166 def format_activity_title(text)
167 167 h(truncate_single_line(text, :length => 100))
168 168 end
169 169
170 170 def format_activity_day(date)
171 171 date == Date.today ? l(:label_today).titleize : format_date(date)
172 172 end
173 173
174 174 def format_activity_description(text)
175 175 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
176 176 end
177 177
178 178 def format_version_name(version)
179 179 if version.project == @project
180 180 h(version)
181 181 else
182 182 h("#{version.project} - #{version}")
183 183 end
184 184 end
185 185
186 186 def due_date_distance_in_words(date)
187 187 if date
188 188 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
189 189 end
190 190 end
191 191
192 192 def render_page_hierarchy(pages, node=nil, options={})
193 193 content = ''
194 194 if pages[node]
195 195 content << "<ul class=\"pages-hierarchy\">\n"
196 196 pages[node].each do |page|
197 197 content << "<li>"
198 198 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
199 199 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
200 200 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
201 201 content << "</li>\n"
202 202 end
203 203 content << "</ul>\n"
204 204 end
205 205 content.html_safe
206 206 end
207 207
208 208 # Renders flash messages
209 209 def render_flash_messages
210 210 s = ''
211 211 flash.each do |k,v|
212 212 s << content_tag('div', v, :class => "flash #{k}")
213 213 end
214 214 s.html_safe
215 215 end
216 216
217 217 # Renders tabs and their content
218 218 def render_tabs(tabs)
219 219 if tabs.any?
220 220 render :partial => 'common/tabs', :locals => {:tabs => tabs}
221 221 else
222 222 content_tag 'p', l(:label_no_data), :class => "nodata"
223 223 end
224 224 end
225 225
226 226 # Renders the project quick-jump box
227 227 def render_project_jump_box
228 228 return unless User.current.logged?
229 229 projects = User.current.memberships.collect(&:project).compact.uniq
230 230 if projects.any?
231 231 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
232 232 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
233 233 '<option value="" disabled="disabled">---</option>'
234 234 s << project_tree_options_for_select(projects, :selected => @project) do |p|
235 235 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
236 236 end
237 237 s << '</select>'
238 238 s.html_safe
239 239 end
240 240 end
241 241
242 242 def project_tree_options_for_select(projects, options = {})
243 243 s = ''
244 244 project_tree(projects) do |project, level|
245 245 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
246 246 tag_options = {:value => project.id}
247 247 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
248 248 tag_options[:selected] = 'selected'
249 249 else
250 250 tag_options[:selected] = nil
251 251 end
252 252 tag_options.merge!(yield(project)) if block_given?
253 253 s << content_tag('option', name_prefix + h(project), tag_options)
254 254 end
255 255 s.html_safe
256 256 end
257 257
258 258 # Yields the given block for each project with its level in the tree
259 259 #
260 260 # Wrapper for Project#project_tree
261 261 def project_tree(projects, &block)
262 262 Project.project_tree(projects, &block)
263 263 end
264 264
265 265 def project_nested_ul(projects, &block)
266 266 s = ''
267 267 if projects.any?
268 268 ancestors = []
269 269 projects.sort_by(&:lft).each do |project|
270 270 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
271 271 s << "<ul>\n"
272 272 else
273 273 ancestors.pop
274 274 s << "</li>"
275 275 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
276 276 ancestors.pop
277 277 s << "</ul></li>\n"
278 278 end
279 279 end
280 280 s << "<li>"
281 281 s << yield(project).to_s
282 282 ancestors << project
283 283 end
284 284 s << ("</li></ul>\n" * ancestors.size)
285 285 end
286 286 s.html_safe
287 287 end
288 288
289 289 def principals_check_box_tags(name, principals)
290 290 s = ''
291 291 principals.sort.each do |principal|
292 292 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
293 293 end
294 294 s.html_safe
295 295 end
296 296
297 297 # Returns a string for users/groups option tags
298 298 def principals_options_for_select(collection, selected=nil)
299 299 s = ''
300 300 groups = ''
301 301 collection.sort.each do |element|
302 302 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected)
303 303 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
304 304 end
305 305 unless groups.empty?
306 306 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
307 307 end
308 308 s
309 309 end
310 310
311 311 # Truncates and returns the string as a single line
312 312 def truncate_single_line(string, *args)
313 313 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
314 314 end
315 315
316 316 # Truncates at line break after 250 characters or options[:length]
317 317 def truncate_lines(string, options={})
318 318 length = options[:length] || 250
319 319 if string.to_s =~ /\A(.{#{length}}.*?)$/m
320 320 "#{$1}..."
321 321 else
322 322 string
323 323 end
324 324 end
325 325
326 326 def html_hours(text)
327 327 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
328 328 end
329 329
330 330 def authoring(created, author, options={})
331 331 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
332 332 end
333 333
334 334 def time_tag(time)
335 335 text = distance_of_time_in_words(Time.now, time)
336 336 if @project
337 337 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
338 338 else
339 339 content_tag('acronym', text, :title => format_time(time))
340 340 end
341 341 end
342 342
343 343 def syntax_highlight(name, content)
344 344 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
345 345 end
346 346
347 347 def to_path_param(path)
348 348 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
349 349 end
350 350
351 351 def pagination_links_full(paginator, count=nil, options={})
352 352 page_param = options.delete(:page_param) || :page
353 353 per_page_links = options.delete(:per_page_links)
354 354 url_param = params.dup
355 355
356 356 html = ''
357 357 if paginator.current.previous
358 358 # \xc2\xab(utf-8) = &#171;
359 359 html << link_to_content_update(
360 360 "\xc2\xab " + l(:label_previous),
361 361 url_param.merge(page_param => paginator.current.previous)) + ' '
362 362 end
363 363
364 364 html << (pagination_links_each(paginator, options) do |n|
365 365 link_to_content_update(n.to_s, url_param.merge(page_param => n))
366 366 end || '')
367 367
368 368 if paginator.current.next
369 369 # \xc2\xbb(utf-8) = &#187;
370 370 html << ' ' + link_to_content_update(
371 371 (l(:label_next) + " \xc2\xbb"),
372 372 url_param.merge(page_param => paginator.current.next))
373 373 end
374 374
375 375 unless count.nil?
376 376 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
377 377 if per_page_links != false && links = per_page_links(paginator.items_per_page)
378 378 html << " | #{links}"
379 379 end
380 380 end
381 381
382 382 html.html_safe
383 383 end
384 384
385 385 def per_page_links(selected=nil)
386 386 links = Setting.per_page_options_array.collect do |n|
387 387 n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
388 388 end
389 389 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
390 390 end
391 391
392 392 def reorder_links(name, url)
393 393 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
394 394 url.merge({"#{name}[move_to]" => 'highest'}),
395 395 :method => :post, :title => l(:label_sort_highest)) +
396 396 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
397 397 url.merge({"#{name}[move_to]" => 'higher'}),
398 398 :method => :post, :title => l(:label_sort_higher)) +
399 399 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
400 400 url.merge({"#{name}[move_to]" => 'lower'}),
401 401 :method => :post, :title => l(:label_sort_lower)) +
402 402 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
403 403 url.merge({"#{name}[move_to]" => 'lowest'}),
404 404 :method => :post, :title => l(:label_sort_lowest))
405 405 end
406 406
407 407 def breadcrumb(*args)
408 408 elements = args.flatten
409 409 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
410 410 end
411 411
412 412 def other_formats_links(&block)
413 413 concat('<p class="other-formats">' + l(:label_export_to))
414 414 yield Redmine::Views::OtherFormatsBuilder.new(self)
415 415 concat('</p>')
416 416 end
417 417
418 418 def page_header_title
419 419 if @project.nil? || @project.new_record?
420 420 h(Setting.app_title)
421 421 else
422 422 b = []
423 423 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
424 424 if ancestors.any?
425 425 root = ancestors.shift
426 426 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
427 427 if ancestors.size > 2
428 428 b << "\xe2\x80\xa6"
429 429 ancestors = ancestors[-2, 2]
430 430 end
431 431 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
432 432 end
433 433 b << h(@project)
434 434 b.join(" \xc2\xbb ").html_safe
435 435 end
436 436 end
437 437
438 438 def html_title(*args)
439 439 if args.empty?
440 440 title = []
441 441 title << @project.name if @project
442 442 title += @html_title if @html_title
443 443 title << Setting.app_title
444 444 title.select {|t| !t.blank? }.join(' - ')
445 445 else
446 446 @html_title ||= []
447 447 @html_title += args
448 448 end
449 449 end
450 450
451 451 # Returns the theme, controller name, and action as css classes for the
452 452 # HTML body.
453 453 def body_css_classes
454 454 css = []
455 455 if theme = Redmine::Themes.theme(Setting.ui_theme)
456 456 css << 'theme-' + theme.name
457 457 end
458 458
459 459 css << 'controller-' + params[:controller]
460 460 css << 'action-' + params[:action]
461 461 css.join(' ')
462 462 end
463 463
464 464 def accesskey(s)
465 465 Redmine::AccessKeys.key_for s
466 466 end
467 467
468 468 # Formats text according to system settings.
469 469 # 2 ways to call this method:
470 470 # * with a String: textilizable(text, options)
471 471 # * with an object and one of its attribute: textilizable(issue, :description, options)
472 472 def textilizable(*args)
473 473 options = args.last.is_a?(Hash) ? args.pop : {}
474 474 case args.size
475 475 when 1
476 476 obj = options[:object]
477 477 text = args.shift
478 478 when 2
479 479 obj = args.shift
480 480 attr = args.shift
481 481 text = obj.send(attr).to_s
482 482 else
483 483 raise ArgumentError, 'invalid arguments to textilizable'
484 484 end
485 485 return '' if text.blank?
486 486 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
487 487 only_path = options.delete(:only_path) == false ? false : true
488 488
489 489 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
490 490
491 491 @parsed_headings = []
492 @current_section = 0 if options[:edit_section_links]
492 493 text = parse_non_pre_blocks(text) do |text|
493 494 [:parse_sections, :parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_macros, :parse_headings].each do |method_name|
494 495 send method_name, text, project, obj, attr, only_path, options
495 496 end
496 497 end
497 498
498 499 if @parsed_headings.any?
499 500 replace_toc(text, @parsed_headings)
500 501 end
501 502
502 503 text
503 504 end
504 505
505 506 def parse_non_pre_blocks(text)
506 507 s = StringScanner.new(text)
507 508 tags = []
508 509 parsed = ''
509 510 while !s.eos?
510 511 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
511 512 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
512 513 if tags.empty?
513 514 yield text
514 515 end
515 516 parsed << text
516 517 if tag
517 518 if closing
518 519 if tags.last == tag.downcase
519 520 tags.pop
520 521 end
521 522 else
522 523 tags << tag.downcase
523 524 end
524 525 parsed << full_tag
525 526 end
526 527 end
527 528 # Close any non closing tags
528 529 while tag = tags.pop
529 530 parsed << "</#{tag}>"
530 531 end
531 532 parsed.html_safe
532 533 end
533 534
534 535 def parse_inline_attachments(text, project, obj, attr, only_path, options)
535 536 # when using an image link, try to use an attachment, if possible
536 537 if options[:attachments] || (obj && obj.respond_to?(:attachments))
537 538 attachments = nil
538 539 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
539 540 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
540 541 attachments ||= (options[:attachments] || obj.attachments).sort_by(&:created_on).reverse
541 542 # search for the picture in attachments
542 543 if found = attachments.detect { |att| att.filename.downcase == filename }
543 544 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
544 545 desc = found.description.to_s.gsub('"', '')
545 546 if !desc.blank? && alttext.blank?
546 547 alt = " title=\"#{desc}\" alt=\"#{desc}\""
547 548 end
548 549 "src=\"#{image_url}\"#{alt}".html_safe
549 550 else
550 551 m.html_safe
551 552 end
552 553 end
553 554 end
554 555 end
555 556
556 557 # Wiki links
557 558 #
558 559 # Examples:
559 560 # [[mypage]]
560 561 # [[mypage|mytext]]
561 562 # wiki links can refer other project wikis, using project name or identifier:
562 563 # [[project:]] -> wiki starting page
563 564 # [[project:|mytext]]
564 565 # [[project:mypage]]
565 566 # [[project:mypage|mytext]]
566 567 def parse_wiki_links(text, project, obj, attr, only_path, options)
567 568 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
568 569 link_project = project
569 570 esc, all, page, title = $1, $2, $3, $5
570 571 if esc.nil?
571 572 if page =~ /^([^\:]+)\:(.*)$/
572 573 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
573 574 page = $2
574 575 title ||= $1 if page.blank?
575 576 end
576 577
577 578 if link_project && link_project.wiki
578 579 # extract anchor
579 580 anchor = nil
580 581 if page =~ /^(.+?)\#(.+)$/
581 582 page, anchor = $1, $2
582 583 end
583 584 anchor = sanitize_anchor_name(anchor) if anchor.present?
584 585 # check if page exists
585 586 wiki_page = link_project.wiki.find_page(page)
586 587 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
587 588 "##{anchor}"
588 589 else
589 590 case options[:wiki_links]
590 591 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
591 592 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
592 593 else
593 594 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
594 595 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, :id => wiki_page_id, :anchor => anchor)
595 596 end
596 597 end
597 598 link_to(title || h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
598 599 else
599 600 # project or wiki doesn't exist
600 601 all.html_safe
601 602 end
602 603 else
603 604 all.html_safe
604 605 end
605 606 end
606 607 end
607 608
608 609 # Redmine links
609 610 #
610 611 # Examples:
611 612 # Issues:
612 613 # #52 -> Link to issue #52
613 614 # Changesets:
614 615 # r52 -> Link to revision 52
615 616 # commit:a85130f -> Link to scmid starting with a85130f
616 617 # Documents:
617 618 # document#17 -> Link to document with id 17
618 619 # document:Greetings -> Link to the document with title "Greetings"
619 620 # document:"Some document" -> Link to the document with title "Some document"
620 621 # Versions:
621 622 # version#3 -> Link to version with id 3
622 623 # version:1.0.0 -> Link to version named "1.0.0"
623 624 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
624 625 # Attachments:
625 626 # attachment:file.zip -> Link to the attachment of the current object named file.zip
626 627 # Source files:
627 628 # source:some/file -> Link to the file located at /some/file in the project's repository
628 629 # source:some/file@52 -> Link to the file's revision 52
629 630 # source:some/file#L120 -> Link to line 120 of the file
630 631 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
631 632 # export:some/file -> Force the download of the file
632 633 # Forum messages:
633 634 # message#1218 -> Link to message with id 1218
634 635 #
635 636 # Links can refer other objects from other projects, using project identifier:
636 637 # identifier:r52
637 638 # identifier:document:"Some document"
638 639 # identifier:version:1.0.0
639 640 # identifier:source:some/file
640 641 def parse_redmine_links(text, project, obj, attr, only_path, options)
641 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 643 leading, esc, project_prefix, project_identifier, prefix, sep, identifier = $1, $2, $3, $4, $5, $7 || $9, $8 || $10
643 644 link = nil
644 645 if project_identifier
645 646 project = Project.visible.find_by_identifier(project_identifier)
646 647 end
647 648 if esc.nil?
648 649 if prefix.nil? && sep == 'r'
649 650 # project.changesets.visible raises an SQL error because of a double join on repositories
650 651 if project && project.repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(project.repository.id, identifier))
651 652 link = link_to(h("#{project_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
652 653 :class => 'changeset',
653 654 :title => truncate_single_line(changeset.comments, :length => 100))
654 655 end
655 656 elsif sep == '#'
656 657 oid = identifier.to_i
657 658 case prefix
658 659 when nil
659 660 if issue = Issue.visible.find_by_id(oid, :include => :status)
660 661 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
661 662 :class => issue.css_classes,
662 663 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
663 664 end
664 665 when 'document'
665 666 if document = Document.visible.find_by_id(oid)
666 667 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
667 668 :class => 'document'
668 669 end
669 670 when 'version'
670 671 if version = Version.visible.find_by_id(oid)
671 672 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
672 673 :class => 'version'
673 674 end
674 675 when 'message'
675 676 if message = Message.visible.find_by_id(oid, :include => :parent)
676 677 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
677 678 end
678 679 when 'project'
679 680 if p = Project.visible.find_by_id(oid)
680 681 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
681 682 end
682 683 end
683 684 elsif sep == ':'
684 685 # removes the double quotes if any
685 686 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
686 687 case prefix
687 688 when 'document'
688 689 if project && document = project.documents.visible.find_by_title(name)
689 690 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
690 691 :class => 'document'
691 692 end
692 693 when 'version'
693 694 if project && version = project.versions.visible.find_by_name(name)
694 695 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
695 696 :class => 'version'
696 697 end
697 698 when 'commit'
698 699 if project && project.repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", project.repository.id, "#{name}%"]))
699 700 link = link_to h("#{project_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.identifier},
700 701 :class => 'changeset',
701 702 :title => truncate_single_line(h(changeset.comments), :length => 100)
702 703 end
703 704 when 'source', 'export'
704 705 if project && project.repository && User.current.allowed_to?(:browse_repository, project)
705 706 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
706 707 path, rev, anchor = $1, $3, $5
707 708 link = link_to h("#{project_prefix}#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
708 709 :path => to_path_param(path),
709 710 :rev => rev,
710 711 :anchor => anchor,
711 712 :format => (prefix == 'export' ? 'raw' : nil)},
712 713 :class => (prefix == 'export' ? 'source download' : 'source')
713 714 end
714 715 when 'attachment'
715 716 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
716 717 if attachments && attachment = attachments.detect {|a| a.filename == name }
717 718 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
718 719 :class => 'attachment'
719 720 end
720 721 when 'project'
721 722 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
722 723 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
723 724 end
724 725 end
725 726 end
726 727 end
727 728 (leading + (link || "#{project_prefix}#{prefix}#{sep}#{identifier}")).html_safe
728 729 end
729 730 end
730 731
731 732 HEADING_RE = /(<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>)/i unless const_defined?(:HEADING_RE)
732 733
733 734 def parse_sections(text, project, obj, attr, only_path, options)
734 735 return unless options[:edit_section_links]
735 section = 0
736 736 text.gsub!(HEADING_RE) do
737 section += 1
738 if section > 1
737 @current_section += 1
738 if @current_section > 1
739 739 content_tag('div',
740 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => section)),
740 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
741 741 :class => 'contextual',
742 742 :title => l(:button_edit_section)) + $1
743 743 else
744 744 $1
745 745 end
746 746 end
747 747 end
748 748
749 749 # Headings and TOC
750 750 # Adds ids and links to headings unless options[:headings] is set to false
751 751 def parse_headings(text, project, obj, attr, only_path, options)
752 752 return if options[:headings] == false
753 753
754 754 text.gsub!(HEADING_RE) do
755 755 level, attrs, content = $2.to_i, $3, $4
756 756 item = strip_tags(content).strip
757 757 anchor = sanitize_anchor_name(item)
758 758 # used for single-file wiki export
759 759 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
760 760 @parsed_headings << [level, anchor, item]
761 761 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
762 762 end
763 763 end
764 764
765 765 MACROS_RE = /
766 766 (!)? # escaping
767 767 (
768 768 \{\{ # opening tag
769 769 ([\w]+) # macro name
770 770 (\(([^\}]*)\))? # optional arguments
771 771 \}\} # closing tag
772 772 )
773 773 /x unless const_defined?(:MACROS_RE)
774 774
775 775 # Macros substitution
776 776 def parse_macros(text, project, obj, attr, only_path, options)
777 777 text.gsub!(MACROS_RE) do
778 778 esc, all, macro = $1, $2, $3.downcase
779 779 args = ($5 || '').split(',').each(&:strip)
780 780 if esc.nil?
781 781 begin
782 782 exec_macro(macro, obj, args)
783 783 rescue => e
784 784 "<div class=\"flash error\">Error executing the <strong>#{macro}</strong> macro (#{e})</div>"
785 785 end || all
786 786 else
787 787 all
788 788 end
789 789 end
790 790 end
791 791
792 792 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
793 793
794 794 # Renders the TOC with given headings
795 795 def replace_toc(text, headings)
796 796 text.gsub!(TOC_RE) do
797 797 if headings.empty?
798 798 ''
799 799 else
800 800 div_class = 'toc'
801 801 div_class << ' right' if $1 == '>'
802 802 div_class << ' left' if $1 == '<'
803 803 out = "<ul class=\"#{div_class}\"><li>"
804 804 root = headings.map(&:first).min
805 805 current = root
806 806 started = false
807 807 headings.each do |level, anchor, item|
808 808 if level > current
809 809 out << '<ul><li>' * (level - current)
810 810 elsif level < current
811 811 out << "</li></ul>\n" * (current - level) + "</li><li>"
812 812 elsif started
813 813 out << '</li><li>'
814 814 end
815 815 out << "<a href=\"##{anchor}\">#{item}</a>"
816 816 current = level
817 817 started = true
818 818 end
819 819 out << '</li></ul>' * (current - root)
820 820 out << '</li></ul>'
821 821 end
822 822 end
823 823 end
824 824
825 825 # Same as Rails' simple_format helper without using paragraphs
826 826 def simple_format_without_paragraph(text)
827 827 text.to_s.
828 828 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
829 829 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
830 830 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
831 831 html_safe
832 832 end
833 833
834 834 def lang_options_for_select(blank=true)
835 835 (blank ? [["(auto)", ""]] : []) +
836 836 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
837 837 end
838 838
839 839 def label_tag_for(name, option_tags = nil, options = {})
840 840 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
841 841 content_tag("label", label_text)
842 842 end
843 843
844 844 def labelled_tabular_form_for(name, object, options, &proc)
845 845 options[:html] ||= {}
846 846 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
847 847 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
848 848 end
849 849
850 850 def back_url_hidden_field_tag
851 851 back_url = params[:back_url] || request.env['HTTP_REFERER']
852 852 back_url = CGI.unescape(back_url.to_s)
853 853 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
854 854 end
855 855
856 856 def check_all_links(form_name)
857 857 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
858 858 " | ".html_safe +
859 859 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
860 860 end
861 861
862 862 def progress_bar(pcts, options={})
863 863 pcts = [pcts, pcts] unless pcts.is_a?(Array)
864 864 pcts = pcts.collect(&:round)
865 865 pcts[1] = pcts[1] - pcts[0]
866 866 pcts << (100 - pcts[1] - pcts[0])
867 867 width = options[:width] || '100px;'
868 868 legend = options[:legend] || ''
869 869 content_tag('table',
870 870 content_tag('tr',
871 871 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
872 872 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
873 873 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
874 874 ), :class => 'progress', :style => "width: #{width};").html_safe +
875 875 content_tag('p', legend, :class => 'pourcent').html_safe
876 876 end
877 877
878 878 def checked_image(checked=true)
879 879 if checked
880 880 image_tag 'toggle_check.png'
881 881 end
882 882 end
883 883
884 884 def context_menu(url)
885 885 unless @context_menu_included
886 886 content_for :header_tags do
887 887 javascript_include_tag('context_menu') +
888 888 stylesheet_link_tag('context_menu')
889 889 end
890 890 if l(:direction) == 'rtl'
891 891 content_for :header_tags do
892 892 stylesheet_link_tag('context_menu_rtl')
893 893 end
894 894 end
895 895 @context_menu_included = true
896 896 end
897 897 javascript_tag "new ContextMenu('#{ url_for(url) }')"
898 898 end
899 899
900 900 def context_menu_link(name, url, options={})
901 901 options[:class] ||= ''
902 902 if options.delete(:selected)
903 903 options[:class] << ' icon-checked disabled'
904 904 options[:disabled] = true
905 905 end
906 906 if options.delete(:disabled)
907 907 options.delete(:method)
908 908 options.delete(:confirm)
909 909 options.delete(:onclick)
910 910 options[:class] << ' disabled'
911 911 url = '#'
912 912 end
913 913 link_to h(name), url, options
914 914 end
915 915
916 916 def calendar_for(field_id)
917 917 include_calendar_headers_tags
918 918 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
919 919 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
920 920 end
921 921
922 922 def include_calendar_headers_tags
923 923 unless @calendar_headers_tags_included
924 924 @calendar_headers_tags_included = true
925 925 content_for :header_tags do
926 926 start_of_week = case Setting.start_of_week.to_i
927 927 when 1
928 928 'Calendar._FD = 1;' # Monday
929 929 when 7
930 930 'Calendar._FD = 0;' # Sunday
931 931 when 6
932 932 'Calendar._FD = 6;' # Saturday
933 933 else
934 934 '' # use language
935 935 end
936 936
937 937 javascript_include_tag('calendar/calendar') +
938 938 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
939 939 javascript_tag(start_of_week) +
940 940 javascript_include_tag('calendar/calendar-setup') +
941 941 stylesheet_link_tag('calendar')
942 942 end
943 943 end
944 944 end
945 945
946 946 def content_for(name, content = nil, &block)
947 947 @has_content ||= {}
948 948 @has_content[name] = true
949 949 super(name, content, &block)
950 950 end
951 951
952 952 def has_content?(name)
953 953 (@has_content && @has_content[name]) || false
954 954 end
955 955
956 956 def email_delivery_enabled?
957 957 !!ActionMailer::Base.perform_deliveries
958 958 end
959 959
960 960 # Returns the avatar image tag for the given +user+ if avatars are enabled
961 961 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
962 962 def avatar(user, options = { })
963 963 if Setting.gravatar_enabled?
964 964 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
965 965 email = nil
966 966 if user.respond_to?(:mail)
967 967 email = user.mail
968 968 elsif user.to_s =~ %r{<(.+?)>}
969 969 email = $1
970 970 end
971 971 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
972 972 else
973 973 ''
974 974 end
975 975 end
976 976
977 977 def sanitize_anchor_name(anchor)
978 978 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
979 979 end
980 980
981 981 # Returns the javascript tags that are included in the html layout head
982 982 def javascript_heads
983 983 tags = javascript_include_tag(:defaults)
984 984 unless User.current.pref.warn_on_leaving_unsaved == '0'
985 985 tags << "\n".html_safe + javascript_tag("Event.observe(window, 'load', function(){ new WarnLeavingUnsaved('#{escape_javascript( l(:text_warn_on_leaving_unsaved) )}'); });")
986 986 end
987 987 tags
988 988 end
989 989
990 990 def favicon
991 991 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
992 992 end
993 993
994 994 def robot_exclusion_tag
995 995 '<meta name="robots" content="noindex,follow,noarchive" />'
996 996 end
997 997
998 998 # Returns true if arg is expected in the API response
999 999 def include_in_api_response?(arg)
1000 1000 unless @included_in_api_response
1001 1001 param = params[:include]
1002 1002 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1003 1003 @included_in_api_response.collect!(&:strip)
1004 1004 end
1005 1005 @included_in_api_response.include?(arg.to_s)
1006 1006 end
1007 1007
1008 1008 # Returns options or nil if nometa param or X-Redmine-Nometa header
1009 1009 # was set in the request
1010 1010 def api_meta(options)
1011 1011 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1012 1012 # compatibility mode for activeresource clients that raise
1013 1013 # an error when unserializing an array with attributes
1014 1014 nil
1015 1015 else
1016 1016 options
1017 1017 end
1018 1018 end
1019 1019
1020 1020 private
1021 1021
1022 1022 def wiki_helper
1023 1023 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1024 1024 extend helper
1025 1025 return self
1026 1026 end
1027 1027
1028 1028 def link_to_content_update(text, url_params = {}, html_options = {})
1029 1029 link_to(text, url_params, html_options)
1030 1030 end
1031 1031 end
@@ -1,127 +1,129
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 50 author_id: 1
51 51 comments:
52 52 wiki_contents_005:
53 53 text: |-
54 54 h1. Child page 1
55 55
56 56 This is a child page
57 57 updated_on: 2007-03-08 00:18:07 +01:00
58 58 page_id: 5
59 59 id: 5
60 60 version: 1
61 61 author_id: 1
62 62 comments:
63 63 wiki_contents_006:
64 64 text: |-
65 65 h1. Child page 2
66 66
67 67 This is a child page
68 68 updated_on: 2007-03-08 00:18:07 +01:00
69 69 page_id: 6
70 70 id: 6
71 71 version: 1
72 72 author_id: 1
73 73 comments:
74 74 wiki_contents_007:
75 75 text: This is a child page
76 76 updated_on: 2007-03-08 00:18:07 +01:00
77 77 page_id: 7
78 78 id: 7
79 79 version: 1
80 80 author_id: 1
81 81 comments:
82 82 wiki_contents_008:
83 83 text: This is a parent page
84 84 updated_on: 2007-03-08 00:18:07 +01:00
85 85 page_id: 8
86 86 id: 8
87 87 version: 1
88 88 author_id: 1
89 89 comments:
90 90 wiki_contents_009:
91 91 text: This is a child page
92 92 updated_on: 2007-03-08 00:18:07 +01:00
93 93 page_id: 9
94 94 id: 9
95 95 version: 1
96 96 author_id: 1
97 97 comments:
98 98 wiki_contents_010:
99 99 text: Page with cyrillic title
100 100 updated_on: 2007-03-08 00:18:07 +01:00
101 101 page_id: 10
102 102 id: 10
103 103 version: 1
104 104 author_id: 1
105 105 comments:
106 106 wiki_contents_011:
107 107 text: |-
108 108 h1. Title
109 109
110 110 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
111 111
112 112 h2. Heading 1
113 113
114 @WHATEVER@
115
114 116 Maecenas sed elit sit amet mi accumsan vestibulum non nec velit. Proin porta tincidunt lorem, consequat rhoncus dolor fermentum in.
115 117
116 118 Cras ipsum felis, ultrices at porttitor vel, faucibus eu nunc.
117 119
118 120 h2. Heading 2
119 121
120 122 Morbi facilisis accumsan orci non pharetra.
121 123 updated_on: 2007-03-08 00:18:07 +01:00
122 124 page_id: 11
123 125 id: 11
124 126 version: 3
125 127 author_id: 1
126 128 comments:
127 129
@@ -1,668 +1,682
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 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.expand_path('../../test_helper', __FILE__)
19 19 require 'wiki_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class WikiController; def rescue_action(e) raise e end; end
23 23
24 24 class WikiControllerTest < ActionController::TestCase
25 25 fixtures :projects, :users, :roles, :members, :member_roles,
26 26 :enabled_modules, :wikis, :wiki_pages, :wiki_contents,
27 27 :wiki_content_versions, :attachments
28 28
29 29 def setup
30 30 @controller = WikiController.new
31 31 @request = ActionController::TestRequest.new
32 32 @response = ActionController::TestResponse.new
33 33 User.current = nil
34 34 end
35 35
36 36 def test_show_start_page
37 37 get :show, :project_id => 'ecookbook'
38 38 assert_response :success
39 39 assert_template 'show'
40 40 assert_tag :tag => 'h1', :content => /CookBook documentation/
41 41
42 42 # child_pages macro
43 43 assert_tag :ul, :attributes => { :class => 'pages-hierarchy' },
44 44 :child => { :tag => 'li',
45 45 :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/Page_with_an_inline_image' },
46 46 :content => 'Page with an inline image' } }
47 47 end
48 48
49 49 def test_show_page_with_name
50 50 get :show, :project_id => 1, :id => 'Another_page'
51 51 assert_response :success
52 52 assert_template 'show'
53 53 assert_tag :tag => 'h1', :content => /Another page/
54 54 # Included page with an inline image
55 55 assert_tag :tag => 'p', :content => /This is an inline image/
56 56 assert_tag :tag => 'img', :attributes => { :src => '/attachments/download/3',
57 57 :alt => 'This is a logo' }
58 58 end
59 59
60 60 def test_show_redirected_page
61 61 WikiRedirect.create!(:wiki_id => 1, :title => 'Old_title', :redirects_to => 'Another_page')
62 62
63 63 get :show, :project_id => 'ecookbook', :id => 'Old_title'
64 64 assert_redirected_to '/projects/ecookbook/wiki/Another_page'
65 65 end
66 66
67 67 def test_show_with_sidebar
68 68 page = Project.find(1).wiki.pages.new(:title => 'Sidebar')
69 69 page.content = WikiContent.new(:text => 'Side bar content for test_show_with_sidebar')
70 70 page.save!
71 71
72 72 get :show, :project_id => 1, :id => 'Another_page'
73 73 assert_response :success
74 74 assert_tag :tag => 'div', :attributes => {:id => 'sidebar'},
75 75 :content => /Side bar content for test_show_with_sidebar/
76 76 end
77 77
78 78 def test_show_unexistent_page_without_edit_right
79 79 get :show, :project_id => 1, :id => 'Unexistent page'
80 80 assert_response 404
81 81 end
82 82
83 def test_show_should_display_section_edit_links
84 @request.session[:user_id] = 2
85 get :show, :project_id => 1, :id => 'Page with sections'
86 assert_no_tag 'a', :attributes => {
87 :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=1'
88 }
89 assert_tag 'a', :attributes => {
90 :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=2'
91 }
92 assert_tag 'a', :attributes => {
93 :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=3'
94 }
95 end
96
83 97 def test_show_unexistent_page_with_edit_right
84 98 @request.session[:user_id] = 2
85 99 get :show, :project_id => 1, :id => 'Unexistent page'
86 100 assert_response :success
87 101 assert_template 'edit'
88 102 end
89 103
90 104 def test_create_page
91 105 @request.session[:user_id] = 2
92 106 put :update, :project_id => 1,
93 107 :id => 'New page',
94 108 :content => {:comments => 'Created the page',
95 109 :text => "h1. New page\n\nThis is a new page",
96 110 :version => 0}
97 111 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'New_page'
98 112 page = Project.find(1).wiki.find_page('New page')
99 113 assert !page.new_record?
100 114 assert_not_nil page.content
101 115 assert_equal 'Created the page', page.content.comments
102 116 end
103 117
104 118 def test_create_page_with_attachments
105 119 @request.session[:user_id] = 2
106 120 assert_difference 'WikiPage.count' do
107 121 assert_difference 'Attachment.count' do
108 122 put :update, :project_id => 1,
109 123 :id => 'New page',
110 124 :content => {:comments => 'Created the page',
111 125 :text => "h1. New page\n\nThis is a new page",
112 126 :version => 0},
113 127 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
114 128 end
115 129 end
116 130 page = Project.find(1).wiki.find_page('New page')
117 131 assert_equal 1, page.attachments.count
118 132 assert_equal 'testfile.txt', page.attachments.first.filename
119 133 end
120 134
121 135 def test_edit_page
122 136 @request.session[:user_id] = 2
123 137 get :edit, :project_id => 'ecookbook', :id => 'Another_page'
124 138
125 139 assert_response :success
126 140 assert_template 'edit'
127 141
128 142 assert_tag 'textarea',
129 143 :attributes => { :name => 'content[text]' },
130 144 :content => WikiPage.find_by_title('Another_page').content.text
131 145 end
132 146
133 147 def test_edit_section
134 148 @request.session[:user_id] = 2
135 149 get :edit, :project_id => 'ecookbook', :id => 'Page_with_sections', :section => 2
136 150
137 151 assert_response :success
138 152 assert_template 'edit'
139 153
140 154 page = WikiPage.find_by_title('Page_with_sections')
141 155 section, hash = Redmine::WikiFormatting::Textile::Formatter.new(page.content.text).get_section(2)
142 156
143 157 assert_tag 'textarea',
144 158 :attributes => { :name => 'content[text]' },
145 159 :content => section
146 160 assert_tag 'input',
147 161 :attributes => { :name => 'section', :type => 'hidden', :value => '2' }
148 162 assert_tag 'input',
149 163 :attributes => { :name => 'section_hash', :type => 'hidden', :value => hash }
150 164 end
151 165
152 166 def test_edit_invalid_section_should_respond_with_404
153 167 @request.session[:user_id] = 2
154 168 get :edit, :project_id => 'ecookbook', :id => 'Page_with_sections', :section => 10
155 169
156 170 assert_response 404
157 171 end
158 172
159 173 def test_update_page
160 174 @request.session[:user_id] = 2
161 175 assert_no_difference 'WikiPage.count' do
162 176 assert_no_difference 'WikiContent.count' do
163 177 assert_difference 'WikiContent::Version.count' do
164 178 put :update, :project_id => 1,
165 179 :id => 'Another_page',
166 180 :content => {
167 181 :comments => "my comments",
168 182 :text => "edited",
169 183 :version => 1
170 184 }
171 185 end
172 186 end
173 187 end
174 188 assert_redirected_to '/projects/ecookbook/wiki/Another_page'
175 189
176 190 page = Wiki.find(1).pages.find_by_title('Another_page')
177 191 assert_equal "edited", page.content.text
178 192 assert_equal 2, page.content.version
179 193 assert_equal "my comments", page.content.comments
180 194 end
181 195
182 196 def test_update_page_with_failure
183 197 @request.session[:user_id] = 2
184 198 assert_no_difference 'WikiPage.count' do
185 199 assert_no_difference 'WikiContent.count' do
186 200 assert_no_difference 'WikiContent::Version.count' do
187 201 put :update, :project_id => 1,
188 202 :id => 'Another_page',
189 203 :content => {
190 204 :comments => 'a' * 300, # failure here, comment is too long
191 205 :text => 'edited',
192 206 :version => 1
193 207 }
194 208 end
195 209 end
196 210 end
197 211 assert_response :success
198 212 assert_template 'edit'
199 213
200 214 assert_error_tag :descendant => {:content => /Comment is too long/}
201 215 assert_tag :tag => 'textarea', :attributes => {:id => 'content_text'}, :content => 'edited'
202 216 assert_tag :tag => 'input', :attributes => {:id => 'content_version', :value => '1'}
203 217 end
204 218
205 219 def test_update_stale_page_should_not_raise_an_error
206 220 @request.session[:user_id] = 2
207 221 c = Wiki.find(1).find_page('Another_page').content
208 222 c.text = 'Previous text'
209 223 c.save!
210 224 assert_equal 2, c.version
211 225
212 226 assert_no_difference 'WikiPage.count' do
213 227 assert_no_difference 'WikiContent.count' do
214 228 assert_no_difference 'WikiContent::Version.count' do
215 229 put :update, :project_id => 1,
216 230 :id => 'Another_page',
217 231 :content => {
218 232 :comments => 'My comments',
219 233 :text => 'Text should not be lost',
220 234 :version => 1
221 235 }
222 236 end
223 237 end
224 238 end
225 239 assert_response :success
226 240 assert_template 'edit'
227 241 assert_tag :div,
228 242 :attributes => { :class => /error/ },
229 243 :content => /Data has been updated by another user/
230 244 assert_tag 'textarea',
231 245 :attributes => { :name => 'content[text]' },
232 246 :content => /Text should not be lost/
233 247 assert_tag 'input',
234 248 :attributes => { :name => 'content[comments]', :value => 'My comments' }
235 249
236 250 c.reload
237 251 assert_equal 'Previous text', c.text
238 252 assert_equal 2, c.version
239 253 end
240 254
241 255 def test_update_section
242 256 @request.session[:user_id] = 2
243 257 page = WikiPage.find_by_title('Page_with_sections')
244 258 section, hash = Redmine::WikiFormatting::Textile::Formatter.new(page.content.text).get_section(2)
245 259 text = page.content.text
246 260
247 261 assert_no_difference 'WikiPage.count' do
248 262 assert_no_difference 'WikiContent.count' do
249 263 assert_difference 'WikiContent::Version.count' do
250 264 put :update, :project_id => 1, :id => 'Page_with_sections',
251 265 :content => {
252 266 :text => "New section content",
253 267 :version => 3
254 268 },
255 269 :section => 2,
256 270 :section_hash => hash
257 271 end
258 272 end
259 273 end
260 274 assert_redirected_to '/projects/ecookbook/wiki/Page_with_sections'
261 275 assert_equal Redmine::WikiFormatting::Textile::Formatter.new(text).update_section(2, "New section content"), page.reload.content.text
262 276 end
263 277
264 278 def test_update_section_should_allow_stale_page_update
265 279 @request.session[:user_id] = 2
266 280 page = WikiPage.find_by_title('Page_with_sections')
267 281 section, hash = Redmine::WikiFormatting::Textile::Formatter.new(page.content.text).get_section(2)
268 282 text = page.content.text
269 283
270 284 assert_no_difference 'WikiPage.count' do
271 285 assert_no_difference 'WikiContent.count' do
272 286 assert_difference 'WikiContent::Version.count' do
273 287 put :update, :project_id => 1, :id => 'Page_with_sections',
274 288 :content => {
275 289 :text => "New section content",
276 290 :version => 2 # Current version is 3
277 291 },
278 292 :section => 2,
279 293 :section_hash => hash
280 294 end
281 295 end
282 296 end
283 297 assert_redirected_to '/projects/ecookbook/wiki/Page_with_sections'
284 298 page.reload
285 299 assert_equal Redmine::WikiFormatting::Textile::Formatter.new(text).update_section(2, "New section content"), page.content.text
286 300 assert_equal 4, page.content.version
287 301 end
288 302
289 303 def test_update_section_should_not_allow_stale_section_update
290 304 @request.session[:user_id] = 2
291 305
292 306 assert_no_difference 'WikiPage.count' do
293 307 assert_no_difference 'WikiContent.count' do
294 308 assert_no_difference 'WikiContent::Version.count' do
295 309 put :update, :project_id => 1, :id => 'Page_with_sections',
296 310 :content => {
297 311 :comments => 'My comments',
298 312 :text => "Text should not be lost",
299 313 :version => 3
300 314 },
301 315 :section => 2,
302 316 :section_hash => Digest::MD5.hexdigest("wrong hash")
303 317 end
304 318 end
305 319 end
306 320 assert_response :success
307 321 assert_template 'edit'
308 322 assert_tag :div,
309 323 :attributes => { :class => /error/ },
310 324 :content => /Data has been updated by another user/
311 325 assert_tag 'textarea',
312 326 :attributes => { :name => 'content[text]' },
313 327 :content => /Text should not be lost/
314 328 assert_tag 'input',
315 329 :attributes => { :name => 'content[comments]', :value => 'My comments' }
316 330 end
317 331
318 332 def test_preview
319 333 @request.session[:user_id] = 2
320 334 xhr :post, :preview, :project_id => 1, :id => 'CookBook_documentation',
321 335 :content => { :comments => '',
322 336 :text => 'this is a *previewed text*',
323 337 :version => 3 }
324 338 assert_response :success
325 339 assert_template 'common/_preview'
326 340 assert_tag :tag => 'strong', :content => /previewed text/
327 341 end
328 342
329 343 def test_preview_new_page
330 344 @request.session[:user_id] = 2
331 345 xhr :post, :preview, :project_id => 1, :id => 'New page',
332 346 :content => { :text => 'h1. New page',
333 347 :comments => '',
334 348 :version => 0 }
335 349 assert_response :success
336 350 assert_template 'common/_preview'
337 351 assert_tag :tag => 'h1', :content => /New page/
338 352 end
339 353
340 354 def test_history
341 355 get :history, :project_id => 1, :id => 'CookBook_documentation'
342 356 assert_response :success
343 357 assert_template 'history'
344 358 assert_not_nil assigns(:versions)
345 359 assert_equal 3, assigns(:versions).size
346 360 assert_select "input[type=submit][name=commit]"
347 361 end
348 362
349 363 def test_history_with_one_version
350 364 get :history, :project_id => 1, :id => 'Another_page'
351 365 assert_response :success
352 366 assert_template 'history'
353 367 assert_not_nil assigns(:versions)
354 368 assert_equal 1, assigns(:versions).size
355 369 assert_select "input[type=submit][name=commit]", false
356 370 end
357 371
358 372 def test_diff
359 373 get :diff, :project_id => 1, :id => 'CookBook_documentation', :version => 2, :version_from => 1
360 374 assert_response :success
361 375 assert_template 'diff'
362 376 assert_tag :tag => 'span', :attributes => { :class => 'diff_in'},
363 377 :content => /updated/
364 378 end
365 379
366 380 def test_annotate
367 381 get :annotate, :project_id => 1, :id => 'CookBook_documentation', :version => 2
368 382 assert_response :success
369 383 assert_template 'annotate'
370 384
371 385 # Line 1
372 386 assert_tag :tag => 'tr', :child => {
373 387 :tag => 'th', :attributes => {:class => 'line-num'}, :content => '1', :sibling => {
374 388 :tag => 'td', :attributes => {:class => 'author'}, :content => /John Smith/, :sibling => {
375 389 :tag => 'td', :content => /h1\. CookBook documentation/
376 390 }
377 391 }
378 392 }
379 393
380 394 # Line 5
381 395 assert_tag :tag => 'tr', :child => {
382 396 :tag => 'th', :attributes => {:class => 'line-num'}, :content => '5', :sibling => {
383 397 :tag => 'td', :attributes => {:class => 'author'}, :content => /redMine Admin/, :sibling => {
384 398 :tag => 'td', :content => /Some updated \[\[documentation\]\] here/
385 399 }
386 400 }
387 401 }
388 402 end
389 403
390 404 def test_get_rename
391 405 @request.session[:user_id] = 2
392 406 get :rename, :project_id => 1, :id => 'Another_page'
393 407 assert_response :success
394 408 assert_template 'rename'
395 409 assert_tag 'option',
396 410 :attributes => {:value => ''},
397 411 :content => '',
398 412 :parent => {:tag => 'select', :attributes => {:name => 'wiki_page[parent_id]'}}
399 413 assert_no_tag 'option',
400 414 :attributes => {:selected => 'selected'},
401 415 :parent => {:tag => 'select', :attributes => {:name => 'wiki_page[parent_id]'}}
402 416 end
403 417
404 418 def test_get_rename_child_page
405 419 @request.session[:user_id] = 2
406 420 get :rename, :project_id => 1, :id => 'Child_1'
407 421 assert_response :success
408 422 assert_template 'rename'
409 423 assert_tag 'option',
410 424 :attributes => {:value => ''},
411 425 :content => '',
412 426 :parent => {:tag => 'select', :attributes => {:name => 'wiki_page[parent_id]'}}
413 427 assert_tag 'option',
414 428 :attributes => {:value => '2', :selected => 'selected'},
415 429 :content => /Another page/,
416 430 :parent => {
417 431 :tag => 'select',
418 432 :attributes => {:name => 'wiki_page[parent_id]'}
419 433 }
420 434 end
421 435
422 436 def test_rename_with_redirect
423 437 @request.session[:user_id] = 2
424 438 post :rename, :project_id => 1, :id => 'Another_page',
425 439 :wiki_page => { :title => 'Another renamed page',
426 440 :redirect_existing_links => 1 }
427 441 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_renamed_page'
428 442 wiki = Project.find(1).wiki
429 443 # Check redirects
430 444 assert_not_nil wiki.find_page('Another page')
431 445 assert_nil wiki.find_page('Another page', :with_redirect => false)
432 446 end
433 447
434 448 def test_rename_without_redirect
435 449 @request.session[:user_id] = 2
436 450 post :rename, :project_id => 1, :id => 'Another_page',
437 451 :wiki_page => { :title => 'Another renamed page',
438 452 :redirect_existing_links => "0" }
439 453 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_renamed_page'
440 454 wiki = Project.find(1).wiki
441 455 # Check that there's no redirects
442 456 assert_nil wiki.find_page('Another page')
443 457 end
444 458
445 459 def test_rename_with_parent_assignment
446 460 @request.session[:user_id] = 2
447 461 post :rename, :project_id => 1, :id => 'Another_page',
448 462 :wiki_page => { :title => 'Another page', :redirect_existing_links => "0", :parent_id => '4' }
449 463 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_page'
450 464 assert_equal WikiPage.find(4), WikiPage.find_by_title('Another_page').parent
451 465 end
452 466
453 467 def test_rename_with_parent_unassignment
454 468 @request.session[:user_id] = 2
455 469 post :rename, :project_id => 1, :id => 'Child_1',
456 470 :wiki_page => { :title => 'Child 1', :redirect_existing_links => "0", :parent_id => '' }
457 471 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Child_1'
458 472 assert_nil WikiPage.find_by_title('Child_1').parent
459 473 end
460 474
461 475 def test_destroy_child
462 476 @request.session[:user_id] = 2
463 477 delete :destroy, :project_id => 1, :id => 'Child_1'
464 478 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
465 479 end
466 480
467 481 def test_destroy_parent
468 482 @request.session[:user_id] = 2
469 483 assert_no_difference('WikiPage.count') do
470 484 delete :destroy, :project_id => 1, :id => 'Another_page'
471 485 end
472 486 assert_response :success
473 487 assert_template 'destroy'
474 488 end
475 489
476 490 def test_destroy_parent_with_nullify
477 491 @request.session[:user_id] = 2
478 492 assert_difference('WikiPage.count', -1) do
479 493 delete :destroy, :project_id => 1, :id => 'Another_page', :todo => 'nullify'
480 494 end
481 495 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
482 496 assert_nil WikiPage.find_by_id(2)
483 497 end
484 498
485 499 def test_destroy_parent_with_cascade
486 500 @request.session[:user_id] = 2
487 501 assert_difference('WikiPage.count', -3) do
488 502 delete :destroy, :project_id => 1, :id => 'Another_page', :todo => 'destroy'
489 503 end
490 504 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
491 505 assert_nil WikiPage.find_by_id(2)
492 506 assert_nil WikiPage.find_by_id(5)
493 507 end
494 508
495 509 def test_destroy_parent_with_reassign
496 510 @request.session[:user_id] = 2
497 511 assert_difference('WikiPage.count', -1) do
498 512 delete :destroy, :project_id => 1, :id => 'Another_page', :todo => 'reassign', :reassign_to_id => 1
499 513 end
500 514 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
501 515 assert_nil WikiPage.find_by_id(2)
502 516 assert_equal WikiPage.find(1), WikiPage.find_by_id(5).parent
503 517 end
504 518
505 519 def test_index
506 520 get :index, :project_id => 'ecookbook'
507 521 assert_response :success
508 522 assert_template 'index'
509 523 pages = assigns(:pages)
510 524 assert_not_nil pages
511 525 assert_equal Project.find(1).wiki.pages.size, pages.size
512 526 assert_equal pages.first.content.updated_on, pages.first.updated_on
513 527
514 528 assert_tag :ul, :attributes => { :class => 'pages-hierarchy' },
515 529 :child => { :tag => 'li', :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/CookBook_documentation' },
516 530 :content => 'CookBook documentation' },
517 531 :child => { :tag => 'ul',
518 532 :child => { :tag => 'li',
519 533 :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/Page_with_an_inline_image' },
520 534 :content => 'Page with an inline image' } } } },
521 535 :child => { :tag => 'li', :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/Another_page' },
522 536 :content => 'Another page' } }
523 537 end
524 538
525 539 def test_index_should_include_atom_link
526 540 get :index, :project_id => 'ecookbook'
527 541 assert_tag 'a', :attributes => { :href => '/projects/ecookbook/activity.atom?show_wiki_edits=1'}
528 542 end
529 543
530 544 context "GET :export" do
531 545 context "with an authorized user to export the wiki" do
532 546 setup do
533 547 @request.session[:user_id] = 2
534 548 get :export, :project_id => 'ecookbook'
535 549 end
536 550
537 551 should_respond_with :success
538 552 should_assign_to :pages
539 553 should_respond_with_content_type "text/html"
540 554 should "export all of the wiki pages to a single html file" do
541 555 assert_select "a[name=?]", "CookBook_documentation"
542 556 assert_select "a[name=?]", "Another_page"
543 557 assert_select "a[name=?]", "Page_with_an_inline_image"
544 558 end
545 559
546 560 end
547 561
548 562 context "with an unauthorized user" do
549 563 setup do
550 564 get :export, :project_id => 'ecookbook'
551 565
552 566 should_respond_with :redirect
553 567 should_redirect_to('wiki index') { {:action => 'show', :project_id => @project, :id => nil} }
554 568 end
555 569 end
556 570 end
557 571
558 572 context "GET :date_index" do
559 573 setup do
560 574 get :date_index, :project_id => 'ecookbook'
561 575 end
562 576
563 577 should_respond_with :success
564 578 should_assign_to :pages
565 579 should_assign_to :pages_by_date
566 580 should_render_template 'wiki/date_index'
567 581
568 582 should "include atom link" do
569 583 assert_tag 'a', :attributes => { :href => '/projects/ecookbook/activity.atom?show_wiki_edits=1'}
570 584 end
571 585 end
572 586
573 587 def test_not_found
574 588 get :show, :project_id => 999
575 589 assert_response 404
576 590 end
577 591
578 592 def test_protect_page
579 593 page = WikiPage.find_by_wiki_id_and_title(1, 'Another_page')
580 594 assert !page.protected?
581 595 @request.session[:user_id] = 2
582 596 post :protect, :project_id => 1, :id => page.title, :protected => '1'
583 597 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_page'
584 598 assert page.reload.protected?
585 599 end
586 600
587 601 def test_unprotect_page
588 602 page = WikiPage.find_by_wiki_id_and_title(1, 'CookBook_documentation')
589 603 assert page.protected?
590 604 @request.session[:user_id] = 2
591 605 post :protect, :project_id => 1, :id => page.title, :protected => '0'
592 606 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'CookBook_documentation'
593 607 assert !page.reload.protected?
594 608 end
595 609
596 610 def test_show_page_with_edit_link
597 611 @request.session[:user_id] = 2
598 612 get :show, :project_id => 1
599 613 assert_response :success
600 614 assert_template 'show'
601 615 assert_tag :tag => 'a', :attributes => { :href => '/projects/1/wiki/CookBook_documentation/edit' }
602 616 end
603 617
604 618 def test_show_page_without_edit_link
605 619 @request.session[:user_id] = 4
606 620 get :show, :project_id => 1
607 621 assert_response :success
608 622 assert_template 'show'
609 623 assert_no_tag :tag => 'a', :attributes => { :href => '/projects/1/wiki/CookBook_documentation/edit' }
610 624 end
611 625
612 626 def test_show_pdf
613 627 @request.session[:user_id] = 2
614 628 get :show, :project_id => 1, :format => 'pdf'
615 629 assert_response :success
616 630 assert_not_nil assigns(:page)
617 631 assert_equal 'application/pdf', @response.content_type
618 632 assert_equal 'attachment; filename="CookBook_documentation.pdf"',
619 633 @response.headers['Content-Disposition']
620 634 end
621 635
622 636 def test_show_html
623 637 @request.session[:user_id] = 2
624 638 get :show, :project_id => 1, :format => 'html'
625 639 assert_response :success
626 640 assert_not_nil assigns(:page)
627 641 assert_equal 'text/html', @response.content_type
628 642 assert_equal 'attachment; filename="CookBook_documentation.html"',
629 643 @response.headers['Content-Disposition']
630 644 end
631 645
632 646 def test_show_txt
633 647 @request.session[:user_id] = 2
634 648 get :show, :project_id => 1, :format => 'txt'
635 649 assert_response :success
636 650 assert_not_nil assigns(:page)
637 651 assert_equal 'text/plain', @response.content_type
638 652 assert_equal 'attachment; filename="CookBook_documentation.txt"',
639 653 @response.headers['Content-Disposition']
640 654 end
641 655
642 656 def test_edit_unprotected_page
643 657 # Non members can edit unprotected wiki pages
644 658 @request.session[:user_id] = 4
645 659 get :edit, :project_id => 1, :id => 'Another_page'
646 660 assert_response :success
647 661 assert_template 'edit'
648 662 end
649 663
650 664 def test_edit_protected_page_by_nonmember
651 665 # Non members can't edit protected wiki pages
652 666 @request.session[:user_id] = 4
653 667 get :edit, :project_id => 1, :id => 'CookBook_documentation'
654 668 assert_response 403
655 669 end
656 670
657 671 def test_edit_protected_page_by_member
658 672 @request.session[:user_id] = 2
659 673 get :edit, :project_id => 1, :id => 'CookBook_documentation'
660 674 assert_response :success
661 675 assert_template 'edit'
662 676 end
663 677
664 678 def test_history_of_non_existing_page_should_return_404
665 679 get :history, :project_id => 1, :id => 'Unknown_page'
666 680 assert_response 404
667 681 end
668 682 end
General Comments 0
You need to be logged in to leave comments. Login now