##// END OF EJS Templates
Backported r11157 and r11158 from trunk (#12801)....
Jean-Philippe Lang -
r11001:ed7318fb8d74
parent child
Show More
@@ -1,1285 +1,1286
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2012 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 # Displays a link to user's account page if active
47 47 def link_to_user(user, options={})
48 48 if user.is_a?(User)
49 49 name = h(user.name(options[:format]))
50 50 if user.active? || (User.current.admin? && user.logged?)
51 51 link_to name, user_path(user), :class => user.css_classes
52 52 else
53 53 name
54 54 end
55 55 else
56 56 h(user.to_s)
57 57 end
58 58 end
59 59
60 60 # Displays a link to +issue+ with its subject.
61 61 # Examples:
62 62 #
63 63 # link_to_issue(issue) # => Defect #6: This is the subject
64 64 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
65 65 # link_to_issue(issue, :subject => false) # => Defect #6
66 66 # link_to_issue(issue, :project => true) # => Foo - Defect #6
67 67 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
68 68 #
69 69 def link_to_issue(issue, options={})
70 70 title = nil
71 71 subject = nil
72 72 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
73 73 if options[:subject] == false
74 74 title = truncate(issue.subject, :length => 60)
75 75 else
76 76 subject = issue.subject
77 77 if options[:truncate]
78 78 subject = truncate(subject, :length => options[:truncate])
79 79 end
80 80 end
81 81 s = link_to text, issue_path(issue), :class => issue.css_classes, :title => title
82 82 s << h(": #{subject}") if subject
83 83 s = h("#{issue.project} - ") + s if options[:project]
84 84 s
85 85 end
86 86
87 87 # Generates a link to an attachment.
88 88 # Options:
89 89 # * :text - Link text (default to attachment filename)
90 90 # * :download - Force download (default: false)
91 91 def link_to_attachment(attachment, options={})
92 92 text = options.delete(:text) || attachment.filename
93 93 action = options.delete(:download) ? 'download' : 'show'
94 94 opt_only_path = {}
95 95 opt_only_path[:only_path] = (options[:only_path] == false ? false : true)
96 96 options.delete(:only_path)
97 97 link_to(h(text),
98 98 {:controller => 'attachments', :action => action,
99 99 :id => attachment, :filename => attachment.filename}.merge(opt_only_path),
100 100 options)
101 101 end
102 102
103 103 # Generates a link to a SCM revision
104 104 # Options:
105 105 # * :text - Link text (default to the formatted revision)
106 106 def link_to_revision(revision, repository, options={})
107 107 if repository.is_a?(Project)
108 108 repository = repository.repository
109 109 end
110 110 text = options.delete(:text) || format_revision(revision)
111 111 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
112 112 link_to(
113 113 h(text),
114 114 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
115 115 :title => l(:label_revision_id, format_revision(revision))
116 116 )
117 117 end
118 118
119 119 # Generates a link to a message
120 120 def link_to_message(message, options={}, html_options = nil)
121 121 link_to(
122 122 h(truncate(message.subject, :length => 60)),
123 123 { :controller => 'messages', :action => 'show',
124 124 :board_id => message.board_id,
125 125 :id => (message.parent_id || message.id),
126 126 :r => (message.parent_id && message.id),
127 127 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
128 128 }.merge(options),
129 129 html_options
130 130 )
131 131 end
132 132
133 133 # Generates a link to a project if active
134 134 # Examples:
135 135 #
136 136 # link_to_project(project) # => link to the specified project overview
137 137 # link_to_project(project, :action=>'settings') # => link to project settings
138 138 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
139 139 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
140 140 #
141 141 def link_to_project(project, options={}, html_options = nil)
142 142 if project.archived?
143 143 h(project)
144 144 else
145 145 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
146 146 link_to(h(project), url, html_options)
147 147 end
148 148 end
149 149
150 150 def wiki_page_path(page, options={})
151 151 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
152 152 end
153 153
154 154 def thumbnail_tag(attachment)
155 155 link_to image_tag(url_for(:controller => 'attachments', :action => 'thumbnail', :id => attachment)),
156 156 {:controller => 'attachments', :action => 'show', :id => attachment, :filename => attachment.filename},
157 157 :title => attachment.filename
158 158 end
159 159
160 160 def toggle_link(name, id, options={})
161 161 onclick = "$('##{id}').toggle(); "
162 162 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
163 163 onclick << "return false;"
164 164 link_to(name, "#", :onclick => onclick)
165 165 end
166 166
167 167 def image_to_function(name, function, html_options = {})
168 168 html_options.symbolize_keys!
169 169 tag(:input, html_options.merge({
170 170 :type => "image", :src => image_path(name),
171 171 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
172 172 }))
173 173 end
174 174
175 175 def format_activity_title(text)
176 176 h(truncate_single_line(text, :length => 100))
177 177 end
178 178
179 179 def format_activity_day(date)
180 180 date == User.current.today ? l(:label_today).titleize : format_date(date)
181 181 end
182 182
183 183 def format_activity_description(text)
184 184 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
185 185 ).gsub(/[\r\n]+/, "<br />").html_safe
186 186 end
187 187
188 188 def format_version_name(version)
189 189 if version.project == @project
190 190 h(version)
191 191 else
192 192 h("#{version.project} - #{version}")
193 193 end
194 194 end
195 195
196 196 def due_date_distance_in_words(date)
197 197 if date
198 198 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
199 199 end
200 200 end
201 201
202 202 # Renders a tree of projects as a nested set of unordered lists
203 203 # The given collection may be a subset of the whole project tree
204 204 # (eg. some intermediate nodes are private and can not be seen)
205 205 def render_project_nested_lists(projects)
206 206 s = ''
207 207 if projects.any?
208 208 ancestors = []
209 209 original_project = @project
210 210 projects.sort_by(&:lft).each do |project|
211 211 # set the project environment to please macros.
212 212 @project = project
213 213 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
214 214 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
215 215 else
216 216 ancestors.pop
217 217 s << "</li>"
218 218 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
219 219 ancestors.pop
220 220 s << "</ul></li>\n"
221 221 end
222 222 end
223 223 classes = (ancestors.empty? ? 'root' : 'child')
224 224 s << "<li class='#{classes}'><div class='#{classes}'>"
225 225 s << h(block_given? ? yield(project) : project.name)
226 226 s << "</div>\n"
227 227 ancestors << project
228 228 end
229 229 s << ("</li></ul>\n" * ancestors.size)
230 230 @project = original_project
231 231 end
232 232 s.html_safe
233 233 end
234 234
235 235 def render_page_hierarchy(pages, node=nil, options={})
236 236 content = ''
237 237 if pages[node]
238 238 content << "<ul class=\"pages-hierarchy\">\n"
239 239 pages[node].each do |page|
240 240 content << "<li>"
241 241 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
242 242 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
243 243 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
244 244 content << "</li>\n"
245 245 end
246 246 content << "</ul>\n"
247 247 end
248 248 content.html_safe
249 249 end
250 250
251 251 # Renders flash messages
252 252 def render_flash_messages
253 253 s = ''
254 254 flash.each do |k,v|
255 255 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
256 256 end
257 257 s.html_safe
258 258 end
259 259
260 260 # Renders tabs and their content
261 261 def render_tabs(tabs)
262 262 if tabs.any?
263 263 render :partial => 'common/tabs', :locals => {:tabs => tabs}
264 264 else
265 265 content_tag 'p', l(:label_no_data), :class => "nodata"
266 266 end
267 267 end
268 268
269 269 # Renders the project quick-jump box
270 270 def render_project_jump_box
271 271 return unless User.current.logged?
272 272 projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
273 273 if projects.any?
274 274 options =
275 275 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
276 276 '<option value="" disabled="disabled">---</option>').html_safe
277 277
278 278 options << project_tree_options_for_select(projects, :selected => @project) do |p|
279 279 { :value => project_path(:id => p, :jump => current_menu_item) }
280 280 end
281 281
282 282 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
283 283 end
284 284 end
285 285
286 286 def project_tree_options_for_select(projects, options = {})
287 287 s = ''
288 288 project_tree(projects) do |project, level|
289 289 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
290 290 tag_options = {:value => project.id}
291 291 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
292 292 tag_options[:selected] = 'selected'
293 293 else
294 294 tag_options[:selected] = nil
295 295 end
296 296 tag_options.merge!(yield(project)) if block_given?
297 297 s << content_tag('option', name_prefix + h(project), tag_options)
298 298 end
299 299 s.html_safe
300 300 end
301 301
302 302 # Yields the given block for each project with its level in the tree
303 303 #
304 304 # Wrapper for Project#project_tree
305 305 def project_tree(projects, &block)
306 306 Project.project_tree(projects, &block)
307 307 end
308 308
309 309 def principals_check_box_tags(name, principals)
310 310 s = ''
311 311 principals.sort.each do |principal|
312 312 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
313 313 end
314 314 s.html_safe
315 315 end
316 316
317 317 # Returns a string for users/groups option tags
318 318 def principals_options_for_select(collection, selected=nil)
319 319 s = ''
320 320 if collection.include?(User.current)
321 321 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
322 322 end
323 323 groups = ''
324 324 collection.sort.each do |element|
325 325 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected)
326 326 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
327 327 end
328 328 unless groups.empty?
329 329 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
330 330 end
331 331 s.html_safe
332 332 end
333 333
334 334 # Options for the new membership projects combo-box
335 335 def options_for_membership_project_select(principal, projects)
336 336 options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
337 337 options << project_tree_options_for_select(projects) do |p|
338 338 {:disabled => principal.projects.include?(p)}
339 339 end
340 340 options
341 341 end
342 342
343 343 # Truncates and returns the string as a single line
344 344 def truncate_single_line(string, *args)
345 345 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
346 346 end
347 347
348 348 # Truncates at line break after 250 characters or options[:length]
349 349 def truncate_lines(string, options={})
350 350 length = options[:length] || 250
351 351 if string.to_s =~ /\A(.{#{length}}.*?)$/m
352 352 "#{$1}..."
353 353 else
354 354 string
355 355 end
356 356 end
357 357
358 358 def anchor(text)
359 359 text.to_s.gsub(' ', '_')
360 360 end
361 361
362 362 def html_hours(text)
363 363 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
364 364 end
365 365
366 366 def authoring(created, author, options={})
367 367 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
368 368 end
369 369
370 370 def time_tag(time)
371 371 text = distance_of_time_in_words(Time.now, time)
372 372 if @project
373 373 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
374 374 else
375 375 content_tag('acronym', text, :title => format_time(time))
376 376 end
377 377 end
378 378
379 379 def syntax_highlight_lines(name, content)
380 380 lines = []
381 381 syntax_highlight(name, content).each_line { |line| lines << line }
382 382 lines
383 383 end
384 384
385 385 def syntax_highlight(name, content)
386 386 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
387 387 end
388 388
389 389 def to_path_param(path)
390 390 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
391 391 str.blank? ? nil : str
392 392 end
393 393
394 394 def pagination_links_full(paginator, count=nil, options={})
395 395 page_param = options.delete(:page_param) || :page
396 396 per_page_links = options.delete(:per_page_links)
397 397 url_param = params.dup
398 398
399 399 html = ''
400 400 if paginator.current.previous
401 401 # \xc2\xab(utf-8) = &#171;
402 402 html << link_to_content_update(
403 403 "\xc2\xab " + l(:label_previous),
404 404 url_param.merge(page_param => paginator.current.previous)) + ' '
405 405 end
406 406
407 407 html << (pagination_links_each(paginator, options) do |n|
408 408 link_to_content_update(n.to_s, url_param.merge(page_param => n))
409 409 end || '')
410 410
411 411 if paginator.current.next
412 412 # \xc2\xbb(utf-8) = &#187;
413 413 html << ' ' + link_to_content_update(
414 414 (l(:label_next) + " \xc2\xbb"),
415 415 url_param.merge(page_param => paginator.current.next))
416 416 end
417 417
418 418 unless count.nil?
419 419 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
420 420 if per_page_links != false && links = per_page_links(paginator.items_per_page, count)
421 421 html << " | #{links}"
422 422 end
423 423 end
424 424
425 425 html.html_safe
426 426 end
427 427
428 428 def per_page_links(selected=nil, item_count=nil)
429 429 values = Setting.per_page_options_array
430 430 if item_count && values.any?
431 431 if item_count > values.first
432 432 max = values.detect {|value| value >= item_count} || item_count
433 433 else
434 434 max = item_count
435 435 end
436 436 values = values.select {|value| value <= max || value == selected}
437 437 end
438 438 if values.empty? || (values.size == 1 && values.first == selected)
439 439 return nil
440 440 end
441 441 links = values.collect do |n|
442 442 n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
443 443 end
444 444 l(:label_display_per_page, links.join(', '))
445 445 end
446 446
447 447 def reorder_links(name, url, method = :post)
448 448 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
449 449 url.merge({"#{name}[move_to]" => 'highest'}),
450 450 :method => method, :title => l(:label_sort_highest)) +
451 451 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
452 452 url.merge({"#{name}[move_to]" => 'higher'}),
453 453 :method => method, :title => l(:label_sort_higher)) +
454 454 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
455 455 url.merge({"#{name}[move_to]" => 'lower'}),
456 456 :method => method, :title => l(:label_sort_lower)) +
457 457 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
458 458 url.merge({"#{name}[move_to]" => 'lowest'}),
459 459 :method => method, :title => l(:label_sort_lowest))
460 460 end
461 461
462 462 def breadcrumb(*args)
463 463 elements = args.flatten
464 464 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
465 465 end
466 466
467 467 def other_formats_links(&block)
468 468 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
469 469 yield Redmine::Views::OtherFormatsBuilder.new(self)
470 470 concat('</p>'.html_safe)
471 471 end
472 472
473 473 def page_header_title
474 474 if @project.nil? || @project.new_record?
475 475 h(Setting.app_title)
476 476 else
477 477 b = []
478 478 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
479 479 if ancestors.any?
480 480 root = ancestors.shift
481 481 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
482 482 if ancestors.size > 2
483 483 b << "\xe2\x80\xa6"
484 484 ancestors = ancestors[-2, 2]
485 485 end
486 486 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
487 487 end
488 488 b << h(@project)
489 489 b.join(" \xc2\xbb ").html_safe
490 490 end
491 491 end
492 492
493 493 def html_title(*args)
494 494 if args.empty?
495 495 title = @html_title || []
496 496 title << @project.name if @project
497 497 title << Setting.app_title unless Setting.app_title == title.last
498 498 title.select {|t| !t.blank? }.join(' - ')
499 499 else
500 500 @html_title ||= []
501 501 @html_title += args
502 502 end
503 503 end
504 504
505 505 # Returns the theme, controller name, and action as css classes for the
506 506 # HTML body.
507 507 def body_css_classes
508 508 css = []
509 509 if theme = Redmine::Themes.theme(Setting.ui_theme)
510 510 css << 'theme-' + theme.name
511 511 end
512 512
513 513 css << 'controller-' + controller_name
514 514 css << 'action-' + action_name
515 515 css.join(' ')
516 516 end
517 517
518 518 def accesskey(s)
519 519 Redmine::AccessKeys.key_for s
520 520 end
521 521
522 522 # Formats text according to system settings.
523 523 # 2 ways to call this method:
524 524 # * with a String: textilizable(text, options)
525 525 # * with an object and one of its attribute: textilizable(issue, :description, options)
526 526 def textilizable(*args)
527 527 options = args.last.is_a?(Hash) ? args.pop : {}
528 528 case args.size
529 529 when 1
530 530 obj = options[:object]
531 531 text = args.shift
532 532 when 2
533 533 obj = args.shift
534 534 attr = args.shift
535 535 text = obj.send(attr).to_s
536 536 else
537 537 raise ArgumentError, 'invalid arguments to textilizable'
538 538 end
539 539 return '' if text.blank?
540 540 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
541 541 only_path = options.delete(:only_path) == false ? false : true
542 542
543 543 text = text.dup
544 544 macros = catch_macros(text)
545 545 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
546 546
547 547 @parsed_headings = []
548 548 @heading_anchors = {}
549 549 @current_section = 0 if options[:edit_section_links]
550 550
551 551 parse_sections(text, project, obj, attr, only_path, options)
552 552 text = parse_non_pre_blocks(text, obj, macros) do |text|
553 553 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
554 554 send method_name, text, project, obj, attr, only_path, options
555 555 end
556 556 end
557 557 parse_headings(text, project, obj, attr, only_path, options)
558 558
559 559 if @parsed_headings.any?
560 560 replace_toc(text, @parsed_headings)
561 561 end
562 562
563 563 text.html_safe
564 564 end
565 565
566 566 def parse_non_pre_blocks(text, obj, macros)
567 567 s = StringScanner.new(text)
568 568 tags = []
569 569 parsed = ''
570 570 while !s.eos?
571 571 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
572 572 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
573 573 if tags.empty?
574 574 yield text
575 575 inject_macros(text, obj, macros) if macros.any?
576 576 else
577 577 inject_macros(text, obj, macros, false) if macros.any?
578 578 end
579 579 parsed << text
580 580 if tag
581 581 if closing
582 582 if tags.last == tag.downcase
583 583 tags.pop
584 584 end
585 585 else
586 586 tags << tag.downcase
587 587 end
588 588 parsed << full_tag
589 589 end
590 590 end
591 591 # Close any non closing tags
592 592 while tag = tags.pop
593 593 parsed << "</#{tag}>"
594 594 end
595 595 parsed
596 596 end
597 597
598 598 def parse_inline_attachments(text, project, obj, attr, only_path, options)
599 599 # when using an image link, try to use an attachment, if possible
600 if options[:attachments] || (obj && obj.respond_to?(:attachments))
601 attachments = options[:attachments] || obj.attachments
600 attachments = options[:attachments] || []
601 attachments += obj.attachments if obj.respond_to?(:attachments)
602 if attachments.present?
602 603 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
603 604 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
604 605 # search for the picture in attachments
605 606 if found = Attachment.latest_attach(attachments, filename)
606 607 image_url = url_for :only_path => only_path, :controller => 'attachments',
607 608 :action => 'download', :id => found
608 609 desc = found.description.to_s.gsub('"', '')
609 610 if !desc.blank? && alttext.blank?
610 611 alt = " title=\"#{desc}\" alt=\"#{desc}\""
611 612 end
612 613 "src=\"#{image_url}\"#{alt}"
613 614 else
614 615 m
615 616 end
616 617 end
617 618 end
618 619 end
619 620
620 621 # Wiki links
621 622 #
622 623 # Examples:
623 624 # [[mypage]]
624 625 # [[mypage|mytext]]
625 626 # wiki links can refer other project wikis, using project name or identifier:
626 627 # [[project:]] -> wiki starting page
627 628 # [[project:|mytext]]
628 629 # [[project:mypage]]
629 630 # [[project:mypage|mytext]]
630 631 def parse_wiki_links(text, project, obj, attr, only_path, options)
631 632 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
632 633 link_project = project
633 634 esc, all, page, title = $1, $2, $3, $5
634 635 if esc.nil?
635 636 if page =~ /^([^\:]+)\:(.*)$/
636 637 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
637 638 page = $2
638 639 title ||= $1 if page.blank?
639 640 end
640 641
641 642 if link_project && link_project.wiki
642 643 # extract anchor
643 644 anchor = nil
644 645 if page =~ /^(.+?)\#(.+)$/
645 646 page, anchor = $1, $2
646 647 end
647 648 anchor = sanitize_anchor_name(anchor) if anchor.present?
648 649 # check if page exists
649 650 wiki_page = link_project.wiki.find_page(page)
650 651 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
651 652 "##{anchor}"
652 653 else
653 654 case options[:wiki_links]
654 655 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
655 656 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
656 657 else
657 658 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
658 659 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
659 660 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
660 661 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
661 662 end
662 663 end
663 664 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
664 665 else
665 666 # project or wiki doesn't exist
666 667 all
667 668 end
668 669 else
669 670 all
670 671 end
671 672 end
672 673 end
673 674
674 675 # Redmine links
675 676 #
676 677 # Examples:
677 678 # Issues:
678 679 # #52 -> Link to issue #52
679 680 # Changesets:
680 681 # r52 -> Link to revision 52
681 682 # commit:a85130f -> Link to scmid starting with a85130f
682 683 # Documents:
683 684 # document#17 -> Link to document with id 17
684 685 # document:Greetings -> Link to the document with title "Greetings"
685 686 # document:"Some document" -> Link to the document with title "Some document"
686 687 # Versions:
687 688 # version#3 -> Link to version with id 3
688 689 # version:1.0.0 -> Link to version named "1.0.0"
689 690 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
690 691 # Attachments:
691 692 # attachment:file.zip -> Link to the attachment of the current object named file.zip
692 693 # Source files:
693 694 # source:some/file -> Link to the file located at /some/file in the project's repository
694 695 # source:some/file@52 -> Link to the file's revision 52
695 696 # source:some/file#L120 -> Link to line 120 of the file
696 697 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
697 698 # export:some/file -> Force the download of the file
698 699 # Forum messages:
699 700 # message#1218 -> Link to message with id 1218
700 701 #
701 702 # Links can refer other objects from other projects, using project identifier:
702 703 # identifier:r52
703 704 # identifier:document:"Some document"
704 705 # identifier:version:1.0.0
705 706 # identifier:source:some/file
706 707 def parse_redmine_links(text, project, obj, attr, only_path, options)
707 708 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
708 709 leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $2, $3, $4, $5, $10, $11, $8 || $12 || $18, $14 || $19, $15, $17
709 710 link = nil
710 711 if project_identifier
711 712 project = Project.visible.find_by_identifier(project_identifier)
712 713 end
713 714 if esc.nil?
714 715 if prefix.nil? && sep == 'r'
715 716 if project
716 717 repository = nil
717 718 if repo_identifier
718 719 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
719 720 else
720 721 repository = project.repository
721 722 end
722 723 # project.changesets.visible raises an SQL error because of a double join on repositories
723 724 if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier))
724 725 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.revision},
725 726 :class => 'changeset',
726 727 :title => truncate_single_line(changeset.comments, :length => 100))
727 728 end
728 729 end
729 730 elsif sep == '#'
730 731 oid = identifier.to_i
731 732 case prefix
732 733 when nil
733 734 if oid.to_s == identifier && issue = Issue.visible.find_by_id(oid, :include => :status)
734 735 anchor = comment_id ? "note-#{comment_id}" : nil
735 736 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor},
736 737 :class => issue.css_classes,
737 738 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
738 739 end
739 740 when 'document'
740 741 if document = Document.visible.find_by_id(oid)
741 742 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
742 743 :class => 'document'
743 744 end
744 745 when 'version'
745 746 if version = Version.visible.find_by_id(oid)
746 747 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
747 748 :class => 'version'
748 749 end
749 750 when 'message'
750 751 if message = Message.visible.find_by_id(oid, :include => :parent)
751 752 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
752 753 end
753 754 when 'forum'
754 755 if board = Board.visible.find_by_id(oid)
755 756 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
756 757 :class => 'board'
757 758 end
758 759 when 'news'
759 760 if news = News.visible.find_by_id(oid)
760 761 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
761 762 :class => 'news'
762 763 end
763 764 when 'project'
764 765 if p = Project.visible.find_by_id(oid)
765 766 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
766 767 end
767 768 end
768 769 elsif sep == ':'
769 770 # removes the double quotes if any
770 771 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
771 772 case prefix
772 773 when 'document'
773 774 if project && document = project.documents.visible.find_by_title(name)
774 775 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
775 776 :class => 'document'
776 777 end
777 778 when 'version'
778 779 if project && version = project.versions.visible.find_by_name(name)
779 780 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
780 781 :class => 'version'
781 782 end
782 783 when 'forum'
783 784 if project && board = project.boards.visible.find_by_name(name)
784 785 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
785 786 :class => 'board'
786 787 end
787 788 when 'news'
788 789 if project && news = project.news.visible.find_by_title(name)
789 790 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
790 791 :class => 'news'
791 792 end
792 793 when 'commit', 'source', 'export'
793 794 if project
794 795 repository = nil
795 796 if name =~ %r{^(([a-z0-9\-]+)\|)(.+)$}
796 797 repo_prefix, repo_identifier, name = $1, $2, $3
797 798 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
798 799 else
799 800 repository = project.repository
800 801 end
801 802 if prefix == 'commit'
802 803 if repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%"]))
803 804 link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
804 805 :class => 'changeset',
805 806 :title => truncate_single_line(h(changeset.comments), :length => 100)
806 807 end
807 808 else
808 809 if repository && User.current.allowed_to?(:browse_repository, project)
809 810 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
810 811 path, rev, anchor = $1, $3, $5
811 812 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
812 813 :path => to_path_param(path),
813 814 :rev => rev,
814 815 :anchor => anchor},
815 816 :class => (prefix == 'export' ? 'source download' : 'source')
816 817 end
817 818 end
818 819 repo_prefix = nil
819 820 end
820 821 when 'attachment'
821 822 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
822 823 if attachments && attachment = Attachment.latest_attach(attachments, name)
823 824 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
824 825 :class => 'attachment'
825 826 end
826 827 when 'project'
827 828 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
828 829 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
829 830 end
830 831 end
831 832 end
832 833 end
833 834 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
834 835 end
835 836 end
836 837
837 838 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
838 839
839 840 def parse_sections(text, project, obj, attr, only_path, options)
840 841 return unless options[:edit_section_links]
841 842 text.gsub!(HEADING_RE) do
842 843 heading = $1
843 844 @current_section += 1
844 845 if @current_section > 1
845 846 content_tag('div',
846 847 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
847 848 :class => 'contextual',
848 849 :title => l(:button_edit_section)) + heading.html_safe
849 850 else
850 851 heading
851 852 end
852 853 end
853 854 end
854 855
855 856 # Headings and TOC
856 857 # Adds ids and links to headings unless options[:headings] is set to false
857 858 def parse_headings(text, project, obj, attr, only_path, options)
858 859 return if options[:headings] == false
859 860
860 861 text.gsub!(HEADING_RE) do
861 862 level, attrs, content = $2.to_i, $3, $4
862 863 item = strip_tags(content).strip
863 864 anchor = sanitize_anchor_name(item)
864 865 # used for single-file wiki export
865 866 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
866 867 @heading_anchors[anchor] ||= 0
867 868 idx = (@heading_anchors[anchor] += 1)
868 869 if idx > 1
869 870 anchor = "#{anchor}-#{idx}"
870 871 end
871 872 @parsed_headings << [level, anchor, item]
872 873 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
873 874 end
874 875 end
875 876
876 877 MACROS_RE = /(
877 878 (!)? # escaping
878 879 (
879 880 \{\{ # opening tag
880 881 ([\w]+) # macro name
881 882 (\(([^\n\r]*?)\))? # optional arguments
882 883 ([\n\r].*?[\n\r])? # optional block of text
883 884 \}\} # closing tag
884 885 )
885 886 )/mx unless const_defined?(:MACROS_RE)
886 887
887 888 MACRO_SUB_RE = /(
888 889 \{\{
889 890 macro\((\d+)\)
890 891 \}\}
891 892 )/x unless const_defined?(:MACRO_SUB_RE)
892 893
893 894 # Extracts macros from text
894 895 def catch_macros(text)
895 896 macros = {}
896 897 text.gsub!(MACROS_RE) do
897 898 all, macro = $1, $4.downcase
898 899 if macro_exists?(macro) || all =~ MACRO_SUB_RE
899 900 index = macros.size
900 901 macros[index] = all
901 902 "{{macro(#{index})}}"
902 903 else
903 904 all
904 905 end
905 906 end
906 907 macros
907 908 end
908 909
909 910 # Executes and replaces macros in text
910 911 def inject_macros(text, obj, macros, execute=true)
911 912 text.gsub!(MACRO_SUB_RE) do
912 913 all, index = $1, $2.to_i
913 914 orig = macros.delete(index)
914 915 if execute && orig && orig =~ MACROS_RE
915 916 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
916 917 if esc.nil?
917 918 h(exec_macro(macro, obj, args, block) || all)
918 919 else
919 920 h(all)
920 921 end
921 922 elsif orig
922 923 h(orig)
923 924 else
924 925 h(all)
925 926 end
926 927 end
927 928 end
928 929
929 930 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
930 931
931 932 # Renders the TOC with given headings
932 933 def replace_toc(text, headings)
933 934 text.gsub!(TOC_RE) do
934 935 # Keep only the 4 first levels
935 936 headings = headings.select{|level, anchor, item| level <= 4}
936 937 if headings.empty?
937 938 ''
938 939 else
939 940 div_class = 'toc'
940 941 div_class << ' right' if $1 == '>'
941 942 div_class << ' left' if $1 == '<'
942 943 out = "<ul class=\"#{div_class}\"><li>"
943 944 root = headings.map(&:first).min
944 945 current = root
945 946 started = false
946 947 headings.each do |level, anchor, item|
947 948 if level > current
948 949 out << '<ul><li>' * (level - current)
949 950 elsif level < current
950 951 out << "</li></ul>\n" * (current - level) + "</li><li>"
951 952 elsif started
952 953 out << '</li><li>'
953 954 end
954 955 out << "<a href=\"##{anchor}\">#{item}</a>"
955 956 current = level
956 957 started = true
957 958 end
958 959 out << '</li></ul>' * (current - root)
959 960 out << '</li></ul>'
960 961 end
961 962 end
962 963 end
963 964
964 965 # Same as Rails' simple_format helper without using paragraphs
965 966 def simple_format_without_paragraph(text)
966 967 text.to_s.
967 968 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
968 969 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
969 970 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
970 971 html_safe
971 972 end
972 973
973 974 def lang_options_for_select(blank=true)
974 975 (blank ? [["(auto)", ""]] : []) + languages_options
975 976 end
976 977
977 978 def label_tag_for(name, option_tags = nil, options = {})
978 979 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
979 980 content_tag("label", label_text)
980 981 end
981 982
982 983 def labelled_form_for(*args, &proc)
983 984 args << {} unless args.last.is_a?(Hash)
984 985 options = args.last
985 986 if args.first.is_a?(Symbol)
986 987 options.merge!(:as => args.shift)
987 988 end
988 989 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
989 990 form_for(*args, &proc)
990 991 end
991 992
992 993 def labelled_fields_for(*args, &proc)
993 994 args << {} unless args.last.is_a?(Hash)
994 995 options = args.last
995 996 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
996 997 fields_for(*args, &proc)
997 998 end
998 999
999 1000 def labelled_remote_form_for(*args, &proc)
1000 1001 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_remote_form_for is deprecated and will be removed in Redmine 2.2."
1001 1002 args << {} unless args.last.is_a?(Hash)
1002 1003 options = args.last
1003 1004 options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
1004 1005 form_for(*args, &proc)
1005 1006 end
1006 1007
1007 1008 def error_messages_for(*objects)
1008 1009 html = ""
1009 1010 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1010 1011 errors = objects.map {|o| o.errors.full_messages}.flatten
1011 1012 if errors.any?
1012 1013 html << "<div id='errorExplanation'><ul>\n"
1013 1014 errors.each do |error|
1014 1015 html << "<li>#{h error}</li>\n"
1015 1016 end
1016 1017 html << "</ul></div>\n"
1017 1018 end
1018 1019 html.html_safe
1019 1020 end
1020 1021
1021 1022 def delete_link(url, options={})
1022 1023 options = {
1023 1024 :method => :delete,
1024 1025 :data => {:confirm => l(:text_are_you_sure)},
1025 1026 :class => 'icon icon-del'
1026 1027 }.merge(options)
1027 1028
1028 1029 link_to l(:button_delete), url, options
1029 1030 end
1030 1031
1031 1032 def preview_link(url, form, target='preview', options={})
1032 1033 content_tag 'a', l(:label_preview), {
1033 1034 :href => "#",
1034 1035 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1035 1036 :accesskey => accesskey(:preview)
1036 1037 }.merge(options)
1037 1038 end
1038 1039
1039 1040 def link_to_function(name, function, html_options={})
1040 1041 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1041 1042 end
1042 1043
1043 1044 # Helper to render JSON in views
1044 1045 def raw_json(arg)
1045 1046 arg.to_json.to_s.gsub('/', '\/').html_safe
1046 1047 end
1047 1048
1048 1049 def back_url
1049 1050 url = params[:back_url]
1050 1051 if url.nil? && referer = request.env['HTTP_REFERER']
1051 1052 url = CGI.unescape(referer.to_s)
1052 1053 end
1053 1054 url
1054 1055 end
1055 1056
1056 1057 def back_url_hidden_field_tag
1057 1058 url = back_url
1058 1059 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1059 1060 end
1060 1061
1061 1062 def check_all_links(form_name)
1062 1063 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1063 1064 " | ".html_safe +
1064 1065 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1065 1066 end
1066 1067
1067 1068 def progress_bar(pcts, options={})
1068 1069 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1069 1070 pcts = pcts.collect(&:round)
1070 1071 pcts[1] = pcts[1] - pcts[0]
1071 1072 pcts << (100 - pcts[1] - pcts[0])
1072 1073 width = options[:width] || '100px;'
1073 1074 legend = options[:legend] || ''
1074 1075 content_tag('table',
1075 1076 content_tag('tr',
1076 1077 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1077 1078 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1078 1079 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1079 1080 ), :class => 'progress', :style => "width: #{width};").html_safe +
1080 1081 content_tag('p', legend, :class => 'pourcent').html_safe
1081 1082 end
1082 1083
1083 1084 def checked_image(checked=true)
1084 1085 if checked
1085 1086 image_tag 'toggle_check.png'
1086 1087 end
1087 1088 end
1088 1089
1089 1090 def context_menu(url)
1090 1091 unless @context_menu_included
1091 1092 content_for :header_tags do
1092 1093 javascript_include_tag('context_menu') +
1093 1094 stylesheet_link_tag('context_menu')
1094 1095 end
1095 1096 if l(:direction) == 'rtl'
1096 1097 content_for :header_tags do
1097 1098 stylesheet_link_tag('context_menu_rtl')
1098 1099 end
1099 1100 end
1100 1101 @context_menu_included = true
1101 1102 end
1102 1103 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1103 1104 end
1104 1105
1105 1106 def calendar_for(field_id)
1106 1107 include_calendar_headers_tags
1107 1108 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1108 1109 end
1109 1110
1110 1111 def include_calendar_headers_tags
1111 1112 unless @calendar_headers_tags_included
1112 1113 @calendar_headers_tags_included = true
1113 1114 content_for :header_tags do
1114 1115 start_of_week = Setting.start_of_week
1115 1116 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1116 1117 # Redmine uses 1..7 (monday..sunday) in settings and locales
1117 1118 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1118 1119 start_of_week = start_of_week.to_i % 7
1119 1120
1120 1121 tags = javascript_tag(
1121 1122 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1122 1123 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1123 1124 path_to_image('/images/calendar.png') +
1124 1125 "', showButtonPanel: true};")
1125 1126 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1126 1127 unless jquery_locale == 'en'
1127 1128 tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js")
1128 1129 end
1129 1130 tags
1130 1131 end
1131 1132 end
1132 1133 end
1133 1134
1134 1135 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1135 1136 # Examples:
1136 1137 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1137 1138 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1138 1139 #
1139 1140 def stylesheet_link_tag(*sources)
1140 1141 options = sources.last.is_a?(Hash) ? sources.pop : {}
1141 1142 plugin = options.delete(:plugin)
1142 1143 sources = sources.map do |source|
1143 1144 if plugin
1144 1145 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1145 1146 elsif current_theme && current_theme.stylesheets.include?(source)
1146 1147 current_theme.stylesheet_path(source)
1147 1148 else
1148 1149 source
1149 1150 end
1150 1151 end
1151 1152 super sources, options
1152 1153 end
1153 1154
1154 1155 # Overrides Rails' image_tag with themes and plugins support.
1155 1156 # Examples:
1156 1157 # image_tag('image.png') # => picks image.png from the current theme or defaults
1157 1158 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1158 1159 #
1159 1160 def image_tag(source, options={})
1160 1161 if plugin = options.delete(:plugin)
1161 1162 source = "/plugin_assets/#{plugin}/images/#{source}"
1162 1163 elsif current_theme && current_theme.images.include?(source)
1163 1164 source = current_theme.image_path(source)
1164 1165 end
1165 1166 super source, options
1166 1167 end
1167 1168
1168 1169 # Overrides Rails' javascript_include_tag with plugins support
1169 1170 # Examples:
1170 1171 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1171 1172 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1172 1173 #
1173 1174 def javascript_include_tag(*sources)
1174 1175 options = sources.last.is_a?(Hash) ? sources.pop : {}
1175 1176 if plugin = options.delete(:plugin)
1176 1177 sources = sources.map do |source|
1177 1178 if plugin
1178 1179 "/plugin_assets/#{plugin}/javascripts/#{source}"
1179 1180 else
1180 1181 source
1181 1182 end
1182 1183 end
1183 1184 end
1184 1185 super sources, options
1185 1186 end
1186 1187
1187 1188 def content_for(name, content = nil, &block)
1188 1189 @has_content ||= {}
1189 1190 @has_content[name] = true
1190 1191 super(name, content, &block)
1191 1192 end
1192 1193
1193 1194 def has_content?(name)
1194 1195 (@has_content && @has_content[name]) || false
1195 1196 end
1196 1197
1197 1198 def sidebar_content?
1198 1199 has_content?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1199 1200 end
1200 1201
1201 1202 def view_layouts_base_sidebar_hook_response
1202 1203 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1203 1204 end
1204 1205
1205 1206 def email_delivery_enabled?
1206 1207 !!ActionMailer::Base.perform_deliveries
1207 1208 end
1208 1209
1209 1210 # Returns the avatar image tag for the given +user+ if avatars are enabled
1210 1211 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1211 1212 def avatar(user, options = { })
1212 1213 if Setting.gravatar_enabled?
1213 1214 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1214 1215 email = nil
1215 1216 if user.respond_to?(:mail)
1216 1217 email = user.mail
1217 1218 elsif user.to_s =~ %r{<(.+?)>}
1218 1219 email = $1
1219 1220 end
1220 1221 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1221 1222 else
1222 1223 ''
1223 1224 end
1224 1225 end
1225 1226
1226 1227 def sanitize_anchor_name(anchor)
1227 1228 if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java'
1228 1229 anchor.gsub(%r{[^\p{Word}\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1229 1230 else
1230 1231 # TODO: remove when ruby1.8 is no longer supported
1231 1232 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1232 1233 end
1233 1234 end
1234 1235
1235 1236 # Returns the javascript tags that are included in the html layout head
1236 1237 def javascript_heads
1237 1238 tags = javascript_include_tag('jquery-1.7.2-ui-1.8.21-ujs-2.0.3', 'application')
1238 1239 unless User.current.pref.warn_on_leaving_unsaved == '0'
1239 1240 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1240 1241 end
1241 1242 tags
1242 1243 end
1243 1244
1244 1245 def favicon
1245 1246 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1246 1247 end
1247 1248
1248 1249 def robot_exclusion_tag
1249 1250 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1250 1251 end
1251 1252
1252 1253 # Returns true if arg is expected in the API response
1253 1254 def include_in_api_response?(arg)
1254 1255 unless @included_in_api_response
1255 1256 param = params[:include]
1256 1257 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1257 1258 @included_in_api_response.collect!(&:strip)
1258 1259 end
1259 1260 @included_in_api_response.include?(arg.to_s)
1260 1261 end
1261 1262
1262 1263 # Returns options or nil if nometa param or X-Redmine-Nometa header
1263 1264 # was set in the request
1264 1265 def api_meta(options)
1265 1266 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1266 1267 # compatibility mode for activeresource clients that raise
1267 1268 # an error when unserializing an array with attributes
1268 1269 nil
1269 1270 else
1270 1271 options
1271 1272 end
1272 1273 end
1273 1274
1274 1275 private
1275 1276
1276 1277 def wiki_helper
1277 1278 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1278 1279 extend helper
1279 1280 return self
1280 1281 end
1281 1282
1282 1283 def link_to_content_update(text, url_params = {}, html_options = {})
1283 1284 link_to(text, url_params, html_options)
1284 1285 end
1285 1286 end
@@ -1,102 +1,116
1 1 ---
2 2 wiki_content_versions_001:
3 3 updated_on: 2007-03-07 00:08:07 +01:00
4 4 page_id: 1
5 5 id: 1
6 6 version: 1
7 7 author_id: 2
8 8 comments: Page creation
9 9 wiki_content_id: 1
10 10 compression: ""
11 11 data: |-
12 12 h1. CookBook documentation
13 13
14 14
15 15
16 16 Some [[documentation]] here...
17 17 wiki_content_versions_002:
18 18 updated_on: 2007-03-07 00:08:34 +01:00
19 19 page_id: 1
20 20 id: 2
21 21 version: 2
22 22 author_id: 1
23 23 comments: Small update
24 24 wiki_content_id: 1
25 25 compression: ""
26 26 data: |-
27 27 h1. CookBook documentation
28 28
29 29
30 30
31 31 Some updated [[documentation]] here...
32 32 wiki_content_versions_003:
33 33 updated_on: 2007-03-07 00:10:51 +01:00
34 34 page_id: 1
35 35 id: 3
36 36 version: 3
37 37 author_id: 1
38 38 comments: ""
39 39 wiki_content_id: 1
40 40 compression: ""
41 41 data: |-
42 42 h1. CookBook documentation
43 43 Some updated [[documentation]] here...
44 44 wiki_content_versions_004:
45 45 data: |-
46 46 h1. Another page
47 47
48 48 This is a link to a ticket: #2
49 49 updated_on: 2007-03-08 00:18:07 +01:00
50 50 page_id: 2
51 51 wiki_content_id: 2
52 52 id: 4
53 53 version: 1
54 54 author_id: 1
55 55 comments:
56 56 wiki_content_versions_005:
57 57 data: |-
58 58 h1. Title
59 59
60 60 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
61 61
62 62 h2. Heading 1
63 63
64 64 @WHATEVER@
65 65
66 66 Maecenas sed elit sit amet mi accumsan vestibulum non nec velit. Proin porta tincidunt lorem, consequat rhoncus dolor fermentum in.
67 67
68 68 Cras ipsum felis, ultrices at porttitor vel, faucibus eu nunc.
69 69
70 70 h2. Heading 2
71 71
72 72 Morbi facilisis accumsan orci non pharetra.
73 73 updated_on: 2007-03-08 00:16:07 +01:00
74 74 page_id: 11
75 75 wiki_content_id: 11
76 76 id: 5
77 77 version: 2
78 78 author_id: 1
79 79 comments:
80 80 wiki_content_versions_006:
81 81 data: |-
82 82 h1. Title
83 83
84 84 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
85 85
86 86 h2. Heading 1
87 87
88 88 @WHATEVER@
89 89
90 90 Maecenas sed elit sit amet mi accumsan vestibulum non nec velit. Proin porta tincidunt lorem, consequat rhoncus dolor fermentum in.
91 91
92 92 h2. Heading 2
93 93
94 94 Morbi facilisis accumsan orci non pharetra.
95 95 updated_on: 2007-03-08 00:18:07 +01:00
96 96 page_id: 11
97 97 wiki_content_id: 11
98 98 id: 6
99 99 version: 3
100 100 author_id: 1
101 101 comments:
102 wiki_content_versions_007:
103 data: |-
104 h1. Page with an inline image
105
106 This is an inline image:
107
108 !logo.gif!
109 updated_on: 2007-03-08 00:18:07 +01:00
110 page_id: 4
111 wiki_content_id: 4
112 id: 7
113 version: 1
114 author_id: 1
115 comments:
102 116
@@ -1,927 +1,940
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 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_export_link
50 50 Role.anonymous.add_permission! :export_wiki_pages
51 51 get :show, :project_id => 'ecookbook'
52 52 assert_response :success
53 53 assert_tag 'a', :attributes => {:href => '/projects/ecookbook/wiki/CookBook_documentation.txt'}
54 54 end
55 55
56 56 def test_show_page_with_name
57 57 get :show, :project_id => 1, :id => 'Another_page'
58 58 assert_response :success
59 59 assert_template 'show'
60 60 assert_tag :tag => 'h1', :content => /Another page/
61 61 # Included page with an inline image
62 62 assert_tag :tag => 'p', :content => /This is an inline image/
63 63 assert_tag :tag => 'img', :attributes => { :src => '/attachments/download/3',
64 64 :alt => 'This is a logo' }
65 65 end
66 66
67 67 def test_show_old_version
68 68 get :show, :project_id => 'ecookbook', :id => 'CookBook_documentation', :version => '2'
69 69 assert_response :success
70 70 assert_template 'show'
71 71
72 72 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/1', :text => /Previous/
73 73 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/2/diff', :text => /diff/
74 74 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/3', :text => /Next/
75 75 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation', :text => /Current version/
76 76 end
77 77
78 def test_show_old_version_with_attachments
79 page = WikiPage.find(4)
80 assert page.attachments.any?
81 content = page.content
82 content.text = "update"
83 content.save!
84
85 get :show, :project_id => 'ecookbook', :id => page.title, :version => '1'
86 assert_kind_of WikiContent::Version, assigns(:content)
87 assert_response :success
88 assert_template 'show'
89 end
90
78 91 def test_show_old_version_without_permission_should_be_denied
79 92 Role.anonymous.remove_permission! :view_wiki_edits
80 93
81 94 get :show, :project_id => 'ecookbook', :id => 'CookBook_documentation', :version => '2'
82 95 assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fprojects%2Fecookbook%2Fwiki%2FCookBook_documentation%2F2'
83 96 end
84 97
85 98 def test_show_first_version
86 99 get :show, :project_id => 'ecookbook', :id => 'CookBook_documentation', :version => '1'
87 100 assert_response :success
88 101 assert_template 'show'
89 102
90 103 assert_select 'a', :text => /Previous/, :count => 0
91 104 assert_select 'a', :text => /diff/, :count => 0
92 105 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/2', :text => /Next/
93 106 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation', :text => /Current version/
94 107 end
95 108
96 109 def test_show_redirected_page
97 110 WikiRedirect.create!(:wiki_id => 1, :title => 'Old_title', :redirects_to => 'Another_page')
98 111
99 112 get :show, :project_id => 'ecookbook', :id => 'Old_title'
100 113 assert_redirected_to '/projects/ecookbook/wiki/Another_page'
101 114 end
102 115
103 116 def test_show_with_sidebar
104 117 page = Project.find(1).wiki.pages.new(:title => 'Sidebar')
105 118 page.content = WikiContent.new(:text => 'Side bar content for test_show_with_sidebar')
106 119 page.save!
107 120
108 121 get :show, :project_id => 1, :id => 'Another_page'
109 122 assert_response :success
110 123 assert_tag :tag => 'div', :attributes => {:id => 'sidebar'},
111 124 :content => /Side bar content for test_show_with_sidebar/
112 125 end
113 126
114 127 def test_show_should_display_section_edit_links
115 128 @request.session[:user_id] = 2
116 129 get :show, :project_id => 1, :id => 'Page with sections'
117 130 assert_no_tag 'a', :attributes => {
118 131 :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=1'
119 132 }
120 133 assert_tag 'a', :attributes => {
121 134 :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=2'
122 135 }
123 136 assert_tag 'a', :attributes => {
124 137 :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=3'
125 138 }
126 139 end
127 140
128 141 def test_show_current_version_should_display_section_edit_links
129 142 @request.session[:user_id] = 2
130 143 get :show, :project_id => 1, :id => 'Page with sections', :version => 3
131 144
132 145 assert_tag 'a', :attributes => {
133 146 :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=2'
134 147 }
135 148 end
136 149
137 150 def test_show_old_version_should_not_display_section_edit_links
138 151 @request.session[:user_id] = 2
139 152 get :show, :project_id => 1, :id => 'Page with sections', :version => 2
140 153
141 154 assert_no_tag 'a', :attributes => {
142 155 :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=2'
143 156 }
144 157 end
145 158
146 159 def test_show_unexistent_page_without_edit_right
147 160 get :show, :project_id => 1, :id => 'Unexistent page'
148 161 assert_response 404
149 162 end
150 163
151 164 def test_show_unexistent_page_with_edit_right
152 165 @request.session[:user_id] = 2
153 166 get :show, :project_id => 1, :id => 'Unexistent page'
154 167 assert_response :success
155 168 assert_template 'edit'
156 169 end
157 170
158 171 def test_show_unexistent_page_with_parent_should_preselect_parent
159 172 @request.session[:user_id] = 2
160 173 get :show, :project_id => 1, :id => 'Unexistent page', :parent => 'Another_page'
161 174 assert_response :success
162 175 assert_template 'edit'
163 176 assert_tag 'select', :attributes => {:name => 'wiki_page[parent_id]'},
164 177 :child => {:tag => 'option', :attributes => {:value => '2', :selected => 'selected'}}
165 178 end
166 179
167 180 def test_show_should_not_show_history_without_permission
168 181 Role.anonymous.remove_permission! :view_wiki_edits
169 182 get :show, :project_id => 1, :id => 'Page with sections', :version => 2
170 183
171 184 assert_response 302
172 185 end
173 186
174 187 def test_create_page
175 188 @request.session[:user_id] = 2
176 189 assert_difference 'WikiPage.count' do
177 190 assert_difference 'WikiContent.count' do
178 191 put :update, :project_id => 1,
179 192 :id => 'New page',
180 193 :content => {:comments => 'Created the page',
181 194 :text => "h1. New page\n\nThis is a new page",
182 195 :version => 0}
183 196 end
184 197 end
185 198 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'New_page'
186 199 page = Project.find(1).wiki.find_page('New page')
187 200 assert !page.new_record?
188 201 assert_not_nil page.content
189 202 assert_nil page.parent
190 203 assert_equal 'Created the page', page.content.comments
191 204 end
192 205
193 206 def test_create_page_with_attachments
194 207 @request.session[:user_id] = 2
195 208 assert_difference 'WikiPage.count' do
196 209 assert_difference 'Attachment.count' do
197 210 put :update, :project_id => 1,
198 211 :id => 'New page',
199 212 :content => {:comments => 'Created the page',
200 213 :text => "h1. New page\n\nThis is a new page",
201 214 :version => 0},
202 215 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
203 216 end
204 217 end
205 218 page = Project.find(1).wiki.find_page('New page')
206 219 assert_equal 1, page.attachments.count
207 220 assert_equal 'testfile.txt', page.attachments.first.filename
208 221 end
209 222
210 223 def test_create_page_with_parent
211 224 @request.session[:user_id] = 2
212 225 assert_difference 'WikiPage.count' do
213 226 put :update, :project_id => 1, :id => 'New page',
214 227 :content => {:text => "h1. New page\n\nThis is a new page", :version => 0},
215 228 :wiki_page => {:parent_id => 2}
216 229 end
217 230 page = Project.find(1).wiki.find_page('New page')
218 231 assert_equal WikiPage.find(2), page.parent
219 232 end
220 233
221 234 def test_edit_page
222 235 @request.session[:user_id] = 2
223 236 get :edit, :project_id => 'ecookbook', :id => 'Another_page'
224 237
225 238 assert_response :success
226 239 assert_template 'edit'
227 240
228 241 assert_tag 'textarea',
229 242 :attributes => { :name => 'content[text]' },
230 243 :content => "\n"+WikiPage.find_by_title('Another_page').content.text
231 244 end
232 245
233 246 def test_edit_section
234 247 @request.session[:user_id] = 2
235 248 get :edit, :project_id => 'ecookbook', :id => 'Page_with_sections', :section => 2
236 249
237 250 assert_response :success
238 251 assert_template 'edit'
239 252
240 253 page = WikiPage.find_by_title('Page_with_sections')
241 254 section, hash = Redmine::WikiFormatting::Textile::Formatter.new(page.content.text).get_section(2)
242 255
243 256 assert_tag 'textarea',
244 257 :attributes => { :name => 'content[text]' },
245 258 :content => "\n"+section
246 259 assert_tag 'input',
247 260 :attributes => { :name => 'section', :type => 'hidden', :value => '2' }
248 261 assert_tag 'input',
249 262 :attributes => { :name => 'section_hash', :type => 'hidden', :value => hash }
250 263 end
251 264
252 265 def test_edit_invalid_section_should_respond_with_404
253 266 @request.session[:user_id] = 2
254 267 get :edit, :project_id => 'ecookbook', :id => 'Page_with_sections', :section => 10
255 268
256 269 assert_response 404
257 270 end
258 271
259 272 def test_update_page
260 273 @request.session[:user_id] = 2
261 274 assert_no_difference 'WikiPage.count' do
262 275 assert_no_difference 'WikiContent.count' do
263 276 assert_difference 'WikiContent::Version.count' do
264 277 put :update, :project_id => 1,
265 278 :id => 'Another_page',
266 279 :content => {
267 280 :comments => "my comments",
268 281 :text => "edited",
269 282 :version => 1
270 283 }
271 284 end
272 285 end
273 286 end
274 287 assert_redirected_to '/projects/ecookbook/wiki/Another_page'
275 288
276 289 page = Wiki.find(1).pages.find_by_title('Another_page')
277 290 assert_equal "edited", page.content.text
278 291 assert_equal 2, page.content.version
279 292 assert_equal "my comments", page.content.comments
280 293 end
281 294
282 295 def test_update_page_with_parent
283 296 @request.session[:user_id] = 2
284 297 assert_no_difference 'WikiPage.count' do
285 298 assert_no_difference 'WikiContent.count' do
286 299 assert_difference 'WikiContent::Version.count' do
287 300 put :update, :project_id => 1,
288 301 :id => 'Another_page',
289 302 :content => {
290 303 :comments => "my comments",
291 304 :text => "edited",
292 305 :version => 1
293 306 },
294 307 :wiki_page => {:parent_id => '1'}
295 308 end
296 309 end
297 310 end
298 311 assert_redirected_to '/projects/ecookbook/wiki/Another_page'
299 312
300 313 page = Wiki.find(1).pages.find_by_title('Another_page')
301 314 assert_equal "edited", page.content.text
302 315 assert_equal 2, page.content.version
303 316 assert_equal "my comments", page.content.comments
304 317 assert_equal WikiPage.find(1), page.parent
305 318 end
306 319
307 320 def test_update_page_with_failure
308 321 @request.session[:user_id] = 2
309 322 assert_no_difference 'WikiPage.count' do
310 323 assert_no_difference 'WikiContent.count' do
311 324 assert_no_difference 'WikiContent::Version.count' do
312 325 put :update, :project_id => 1,
313 326 :id => 'Another_page',
314 327 :content => {
315 328 :comments => 'a' * 300, # failure here, comment is too long
316 329 :text => 'edited',
317 330 :version => 1
318 331 }
319 332 end
320 333 end
321 334 end
322 335 assert_response :success
323 336 assert_template 'edit'
324 337
325 338 assert_error_tag :descendant => {:content => /Comment is too long/}
326 339 assert_tag :tag => 'textarea', :attributes => {:id => 'content_text'}, :content => "\nedited"
327 340 assert_tag :tag => 'input', :attributes => {:id => 'content_version', :value => '1'}
328 341 end
329 342
330 343 def test_update_page_with_parent_change_only_should_not_create_content_version
331 344 @request.session[:user_id] = 2
332 345 assert_no_difference 'WikiPage.count' do
333 346 assert_no_difference 'WikiContent.count' do
334 347 assert_no_difference 'WikiContent::Version.count' do
335 348 put :update, :project_id => 1,
336 349 :id => 'Another_page',
337 350 :content => {
338 351 :comments => '',
339 352 :text => Wiki.find(1).find_page('Another_page').content.text,
340 353 :version => 1
341 354 },
342 355 :wiki_page => {:parent_id => '1'}
343 356 end
344 357 end
345 358 end
346 359 page = Wiki.find(1).pages.find_by_title('Another_page')
347 360 assert_equal 1, page.content.version
348 361 assert_equal WikiPage.find(1), page.parent
349 362 end
350 363
351 364 def test_update_page_with_attachments_only_should_not_create_content_version
352 365 @request.session[:user_id] = 2
353 366 assert_no_difference 'WikiPage.count' do
354 367 assert_no_difference 'WikiContent.count' do
355 368 assert_no_difference 'WikiContent::Version.count' do
356 369 assert_difference 'Attachment.count' do
357 370 put :update, :project_id => 1,
358 371 :id => 'Another_page',
359 372 :content => {
360 373 :comments => '',
361 374 :text => Wiki.find(1).find_page('Another_page').content.text,
362 375 :version => 1
363 376 },
364 377 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
365 378 end
366 379 end
367 380 end
368 381 end
369 382 page = Wiki.find(1).pages.find_by_title('Another_page')
370 383 assert_equal 1, page.content.version
371 384 end
372 385
373 386 def test_update_stale_page_should_not_raise_an_error
374 387 @request.session[:user_id] = 2
375 388 c = Wiki.find(1).find_page('Another_page').content
376 389 c.text = 'Previous text'
377 390 c.save!
378 391 assert_equal 2, c.version
379 392
380 393 assert_no_difference 'WikiPage.count' do
381 394 assert_no_difference 'WikiContent.count' do
382 395 assert_no_difference 'WikiContent::Version.count' do
383 396 put :update, :project_id => 1,
384 397 :id => 'Another_page',
385 398 :content => {
386 399 :comments => 'My comments',
387 400 :text => 'Text should not be lost',
388 401 :version => 1
389 402 }
390 403 end
391 404 end
392 405 end
393 406 assert_response :success
394 407 assert_template 'edit'
395 408 assert_tag :div,
396 409 :attributes => { :class => /error/ },
397 410 :content => /Data has been updated by another user/
398 411 assert_tag 'textarea',
399 412 :attributes => { :name => 'content[text]' },
400 413 :content => /Text should not be lost/
401 414 assert_tag 'input',
402 415 :attributes => { :name => 'content[comments]', :value => 'My comments' }
403 416
404 417 c.reload
405 418 assert_equal 'Previous text', c.text
406 419 assert_equal 2, c.version
407 420 end
408 421
409 422 def test_update_section
410 423 @request.session[:user_id] = 2
411 424 page = WikiPage.find_by_title('Page_with_sections')
412 425 section, hash = Redmine::WikiFormatting::Textile::Formatter.new(page.content.text).get_section(2)
413 426 text = page.content.text
414 427
415 428 assert_no_difference 'WikiPage.count' do
416 429 assert_no_difference 'WikiContent.count' do
417 430 assert_difference 'WikiContent::Version.count' do
418 431 put :update, :project_id => 1, :id => 'Page_with_sections',
419 432 :content => {
420 433 :text => "New section content",
421 434 :version => 3
422 435 },
423 436 :section => 2,
424 437 :section_hash => hash
425 438 end
426 439 end
427 440 end
428 441 assert_redirected_to '/projects/ecookbook/wiki/Page_with_sections'
429 442 assert_equal Redmine::WikiFormatting::Textile::Formatter.new(text).update_section(2, "New section content"), page.reload.content.text
430 443 end
431 444
432 445 def test_update_section_should_allow_stale_page_update
433 446 @request.session[:user_id] = 2
434 447 page = WikiPage.find_by_title('Page_with_sections')
435 448 section, hash = Redmine::WikiFormatting::Textile::Formatter.new(page.content.text).get_section(2)
436 449 text = page.content.text
437 450
438 451 assert_no_difference 'WikiPage.count' do
439 452 assert_no_difference 'WikiContent.count' do
440 453 assert_difference 'WikiContent::Version.count' do
441 454 put :update, :project_id => 1, :id => 'Page_with_sections',
442 455 :content => {
443 456 :text => "New section content",
444 457 :version => 2 # Current version is 3
445 458 },
446 459 :section => 2,
447 460 :section_hash => hash
448 461 end
449 462 end
450 463 end
451 464 assert_redirected_to '/projects/ecookbook/wiki/Page_with_sections'
452 465 page.reload
453 466 assert_equal Redmine::WikiFormatting::Textile::Formatter.new(text).update_section(2, "New section content"), page.content.text
454 467 assert_equal 4, page.content.version
455 468 end
456 469
457 470 def test_update_section_should_not_allow_stale_section_update
458 471 @request.session[:user_id] = 2
459 472
460 473 assert_no_difference 'WikiPage.count' do
461 474 assert_no_difference 'WikiContent.count' do
462 475 assert_no_difference 'WikiContent::Version.count' do
463 476 put :update, :project_id => 1, :id => 'Page_with_sections',
464 477 :content => {
465 478 :comments => 'My comments',
466 479 :text => "Text should not be lost",
467 480 :version => 3
468 481 },
469 482 :section => 2,
470 483 :section_hash => Digest::MD5.hexdigest("wrong hash")
471 484 end
472 485 end
473 486 end
474 487 assert_response :success
475 488 assert_template 'edit'
476 489 assert_tag :div,
477 490 :attributes => { :class => /error/ },
478 491 :content => /Data has been updated by another user/
479 492 assert_tag 'textarea',
480 493 :attributes => { :name => 'content[text]' },
481 494 :content => /Text should not be lost/
482 495 assert_tag 'input',
483 496 :attributes => { :name => 'content[comments]', :value => 'My comments' }
484 497 end
485 498
486 499 def test_preview
487 500 @request.session[:user_id] = 2
488 501 xhr :post, :preview, :project_id => 1, :id => 'CookBook_documentation',
489 502 :content => { :comments => '',
490 503 :text => 'this is a *previewed text*',
491 504 :version => 3 }
492 505 assert_response :success
493 506 assert_template 'common/_preview'
494 507 assert_tag :tag => 'strong', :content => /previewed text/
495 508 end
496 509
497 510 def test_preview_new_page
498 511 @request.session[:user_id] = 2
499 512 xhr :post, :preview, :project_id => 1, :id => 'New page',
500 513 :content => { :text => 'h1. New page',
501 514 :comments => '',
502 515 :version => 0 }
503 516 assert_response :success
504 517 assert_template 'common/_preview'
505 518 assert_tag :tag => 'h1', :content => /New page/
506 519 end
507 520
508 521 def test_history
509 522 @request.session[:user_id] = 2
510 523 get :history, :project_id => 'ecookbook', :id => 'CookBook_documentation'
511 524 assert_response :success
512 525 assert_template 'history'
513 526 assert_not_nil assigns(:versions)
514 527 assert_equal 3, assigns(:versions).size
515 528
516 529 assert_select "input[type=submit][name=commit]"
517 530 assert_select 'td' do
518 531 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/2', :text => '2'
519 532 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/2/annotate', :text => 'Annotate'
520 533 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/2', :text => 'Delete'
521 534 end
522 535 end
523 536
524 537 def test_history_with_one_version
525 538 @request.session[:user_id] = 2
526 539 get :history, :project_id => 'ecookbook', :id => 'Another_page'
527 540 assert_response :success
528 541 assert_template 'history'
529 542 assert_not_nil assigns(:versions)
530 543 assert_equal 1, assigns(:versions).size
531 544 assert_select "input[type=submit][name=commit]", false
532 545 assert_select 'td' do
533 546 assert_select 'a[href=?]', '/projects/ecookbook/wiki/Another_page/1', :text => '1'
534 547 assert_select 'a[href=?]', '/projects/ecookbook/wiki/Another_page/1/annotate', :text => 'Annotate'
535 548 assert_select 'a[href=?]', '/projects/ecookbook/wiki/Another_page/1', :text => 'Delete', :count => 0
536 549 end
537 550 end
538 551
539 552 def test_diff
540 553 content = WikiPage.find(1).content
541 554 assert_difference 'WikiContent::Version.count', 2 do
542 555 content.text = "Line removed\nThis is a sample text for testing diffs"
543 556 content.save!
544 557 content.text = "This is a sample text for testing diffs\nLine added"
545 558 content.save!
546 559 end
547 560
548 561 get :diff, :project_id => 1, :id => 'CookBook_documentation', :version => content.version, :version_from => (content.version - 1)
549 562 assert_response :success
550 563 assert_template 'diff'
551 564 assert_select 'span.diff_out', :text => 'Line removed'
552 565 assert_select 'span.diff_in', :text => 'Line added'
553 566 end
554 567
555 568 def test_diff_with_invalid_version_should_respond_with_404
556 569 get :diff, :project_id => 1, :id => 'CookBook_documentation', :version => '99'
557 570 assert_response 404
558 571 end
559 572
560 573 def test_diff_with_invalid_version_from_should_respond_with_404
561 574 get :diff, :project_id => 1, :id => 'CookBook_documentation', :version => '99', :version_from => '98'
562 575 assert_response 404
563 576 end
564 577
565 578 def test_annotate
566 579 get :annotate, :project_id => 1, :id => 'CookBook_documentation', :version => 2
567 580 assert_response :success
568 581 assert_template 'annotate'
569 582
570 583 # Line 1
571 584 assert_tag :tag => 'tr', :child => {
572 585 :tag => 'th', :attributes => {:class => 'line-num'}, :content => '1', :sibling => {
573 586 :tag => 'td', :attributes => {:class => 'author'}, :content => /John Smith/, :sibling => {
574 587 :tag => 'td', :content => /h1\. CookBook documentation/
575 588 }
576 589 }
577 590 }
578 591
579 592 # Line 5
580 593 assert_tag :tag => 'tr', :child => {
581 594 :tag => 'th', :attributes => {:class => 'line-num'}, :content => '5', :sibling => {
582 595 :tag => 'td', :attributes => {:class => 'author'}, :content => /redMine Admin/, :sibling => {
583 596 :tag => 'td', :content => /Some updated \[\[documentation\]\] here/
584 597 }
585 598 }
586 599 }
587 600 end
588 601
589 602 def test_annotate_with_invalid_version_should_respond_with_404
590 603 get :annotate, :project_id => 1, :id => 'CookBook_documentation', :version => '99'
591 604 assert_response 404
592 605 end
593 606
594 607 def test_get_rename
595 608 @request.session[:user_id] = 2
596 609 get :rename, :project_id => 1, :id => 'Another_page'
597 610 assert_response :success
598 611 assert_template 'rename'
599 612 assert_tag 'option',
600 613 :attributes => {:value => ''},
601 614 :content => '',
602 615 :parent => {:tag => 'select', :attributes => {:name => 'wiki_page[parent_id]'}}
603 616 assert_no_tag 'option',
604 617 :attributes => {:selected => 'selected'},
605 618 :parent => {:tag => 'select', :attributes => {:name => 'wiki_page[parent_id]'}}
606 619 end
607 620
608 621 def test_get_rename_child_page
609 622 @request.session[:user_id] = 2
610 623 get :rename, :project_id => 1, :id => 'Child_1'
611 624 assert_response :success
612 625 assert_template 'rename'
613 626 assert_tag 'option',
614 627 :attributes => {:value => ''},
615 628 :content => '',
616 629 :parent => {:tag => 'select', :attributes => {:name => 'wiki_page[parent_id]'}}
617 630 assert_tag 'option',
618 631 :attributes => {:value => '2', :selected => 'selected'},
619 632 :content => /Another page/,
620 633 :parent => {
621 634 :tag => 'select',
622 635 :attributes => {:name => 'wiki_page[parent_id]'}
623 636 }
624 637 end
625 638
626 639 def test_rename_with_redirect
627 640 @request.session[:user_id] = 2
628 641 post :rename, :project_id => 1, :id => 'Another_page',
629 642 :wiki_page => { :title => 'Another renamed page',
630 643 :redirect_existing_links => 1 }
631 644 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_renamed_page'
632 645 wiki = Project.find(1).wiki
633 646 # Check redirects
634 647 assert_not_nil wiki.find_page('Another page')
635 648 assert_nil wiki.find_page('Another page', :with_redirect => false)
636 649 end
637 650
638 651 def test_rename_without_redirect
639 652 @request.session[:user_id] = 2
640 653 post :rename, :project_id => 1, :id => 'Another_page',
641 654 :wiki_page => { :title => 'Another renamed page',
642 655 :redirect_existing_links => "0" }
643 656 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_renamed_page'
644 657 wiki = Project.find(1).wiki
645 658 # Check that there's no redirects
646 659 assert_nil wiki.find_page('Another page')
647 660 end
648 661
649 662 def test_rename_with_parent_assignment
650 663 @request.session[:user_id] = 2
651 664 post :rename, :project_id => 1, :id => 'Another_page',
652 665 :wiki_page => { :title => 'Another page', :redirect_existing_links => "0", :parent_id => '4' }
653 666 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_page'
654 667 assert_equal WikiPage.find(4), WikiPage.find_by_title('Another_page').parent
655 668 end
656 669
657 670 def test_rename_with_parent_unassignment
658 671 @request.session[:user_id] = 2
659 672 post :rename, :project_id => 1, :id => 'Child_1',
660 673 :wiki_page => { :title => 'Child 1', :redirect_existing_links => "0", :parent_id => '' }
661 674 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Child_1'
662 675 assert_nil WikiPage.find_by_title('Child_1').parent
663 676 end
664 677
665 678 def test_destroy_a_page_without_children_should_not_ask_confirmation
666 679 @request.session[:user_id] = 2
667 680 delete :destroy, :project_id => 1, :id => 'Child_2'
668 681 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
669 682 end
670 683
671 684 def test_destroy_parent_should_ask_confirmation
672 685 @request.session[:user_id] = 2
673 686 assert_no_difference('WikiPage.count') do
674 687 delete :destroy, :project_id => 1, :id => 'Another_page'
675 688 end
676 689 assert_response :success
677 690 assert_template 'destroy'
678 691 assert_select 'form' do
679 692 assert_select 'input[name=todo][value=nullify]'
680 693 assert_select 'input[name=todo][value=destroy]'
681 694 assert_select 'input[name=todo][value=reassign]'
682 695 end
683 696 end
684 697
685 698 def test_destroy_parent_with_nullify_should_delete_parent_only
686 699 @request.session[:user_id] = 2
687 700 assert_difference('WikiPage.count', -1) do
688 701 delete :destroy, :project_id => 1, :id => 'Another_page', :todo => 'nullify'
689 702 end
690 703 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
691 704 assert_nil WikiPage.find_by_id(2)
692 705 end
693 706
694 707 def test_destroy_parent_with_cascade_should_delete_descendants
695 708 @request.session[:user_id] = 2
696 709 assert_difference('WikiPage.count', -4) do
697 710 delete :destroy, :project_id => 1, :id => 'Another_page', :todo => 'destroy'
698 711 end
699 712 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
700 713 assert_nil WikiPage.find_by_id(2)
701 714 assert_nil WikiPage.find_by_id(5)
702 715 end
703 716
704 717 def test_destroy_parent_with_reassign
705 718 @request.session[:user_id] = 2
706 719 assert_difference('WikiPage.count', -1) do
707 720 delete :destroy, :project_id => 1, :id => 'Another_page', :todo => 'reassign', :reassign_to_id => 1
708 721 end
709 722 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
710 723 assert_nil WikiPage.find_by_id(2)
711 724 assert_equal WikiPage.find(1), WikiPage.find_by_id(5).parent
712 725 end
713 726
714 727 def test_destroy_version
715 728 @request.session[:user_id] = 2
716 729 assert_difference 'WikiContent::Version.count', -1 do
717 730 assert_no_difference 'WikiContent.count' do
718 731 assert_no_difference 'WikiPage.count' do
719 732 delete :destroy_version, :project_id => 'ecookbook', :id => 'CookBook_documentation', :version => 2
720 733 assert_redirected_to '/projects/ecookbook/wiki/CookBook_documentation/history'
721 734 end
722 735 end
723 736 end
724 737 end
725 738
726 739 def test_index
727 740 get :index, :project_id => 'ecookbook'
728 741 assert_response :success
729 742 assert_template 'index'
730 743 pages = assigns(:pages)
731 744 assert_not_nil pages
732 745 assert_equal Project.find(1).wiki.pages.size, pages.size
733 746 assert_equal pages.first.content.updated_on, pages.first.updated_on
734 747
735 748 assert_tag :ul, :attributes => { :class => 'pages-hierarchy' },
736 749 :child => { :tag => 'li', :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/CookBook_documentation' },
737 750 :content => 'CookBook documentation' },
738 751 :child => { :tag => 'ul',
739 752 :child => { :tag => 'li',
740 753 :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/Page_with_an_inline_image' },
741 754 :content => 'Page with an inline image' } } } },
742 755 :child => { :tag => 'li', :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/Another_page' },
743 756 :content => 'Another page' } }
744 757 end
745 758
746 759 def test_index_should_include_atom_link
747 760 get :index, :project_id => 'ecookbook'
748 761 assert_tag 'a', :attributes => { :href => '/projects/ecookbook/activity.atom?show_wiki_edits=1'}
749 762 end
750 763
751 764 def test_export_to_html
752 765 @request.session[:user_id] = 2
753 766 get :export, :project_id => 'ecookbook'
754 767
755 768 assert_response :success
756 769 assert_not_nil assigns(:pages)
757 770 assert assigns(:pages).any?
758 771 assert_equal "text/html", @response.content_type
759 772
760 773 assert_select "a[name=?]", "CookBook_documentation"
761 774 assert_select "a[name=?]", "Another_page"
762 775 assert_select "a[name=?]", "Page_with_an_inline_image"
763 776 end
764 777
765 778 def test_export_to_pdf
766 779 @request.session[:user_id] = 2
767 780 get :export, :project_id => 'ecookbook', :format => 'pdf'
768 781
769 782 assert_response :success
770 783 assert_not_nil assigns(:pages)
771 784 assert assigns(:pages).any?
772 785 assert_equal 'application/pdf', @response.content_type
773 786 assert_equal 'attachment; filename="ecookbook.pdf"', @response.headers['Content-Disposition']
774 787 assert @response.body.starts_with?('%PDF')
775 788 end
776 789
777 790 def test_export_without_permission_should_be_denied
778 791 @request.session[:user_id] = 2
779 792 Role.find_by_name('Manager').remove_permission! :export_wiki_pages
780 793 get :export, :project_id => 'ecookbook'
781 794
782 795 assert_response 403
783 796 end
784 797
785 798 def test_date_index
786 799 get :date_index, :project_id => 'ecookbook'
787 800
788 801 assert_response :success
789 802 assert_template 'date_index'
790 803 assert_not_nil assigns(:pages)
791 804 assert_not_nil assigns(:pages_by_date)
792 805
793 806 assert_tag 'a', :attributes => { :href => '/projects/ecookbook/activity.atom?show_wiki_edits=1'}
794 807 end
795 808
796 809 def test_not_found
797 810 get :show, :project_id => 999
798 811 assert_response 404
799 812 end
800 813
801 814 def test_protect_page
802 815 page = WikiPage.find_by_wiki_id_and_title(1, 'Another_page')
803 816 assert !page.protected?
804 817 @request.session[:user_id] = 2
805 818 post :protect, :project_id => 1, :id => page.title, :protected => '1'
806 819 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_page'
807 820 assert page.reload.protected?
808 821 end
809 822
810 823 def test_unprotect_page
811 824 page = WikiPage.find_by_wiki_id_and_title(1, 'CookBook_documentation')
812 825 assert page.protected?
813 826 @request.session[:user_id] = 2
814 827 post :protect, :project_id => 1, :id => page.title, :protected => '0'
815 828 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'CookBook_documentation'
816 829 assert !page.reload.protected?
817 830 end
818 831
819 832 def test_show_page_with_edit_link
820 833 @request.session[:user_id] = 2
821 834 get :show, :project_id => 1
822 835 assert_response :success
823 836 assert_template 'show'
824 837 assert_tag :tag => 'a', :attributes => { :href => '/projects/1/wiki/CookBook_documentation/edit' }
825 838 end
826 839
827 840 def test_show_page_without_edit_link
828 841 @request.session[:user_id] = 4
829 842 get :show, :project_id => 1
830 843 assert_response :success
831 844 assert_template 'show'
832 845 assert_no_tag :tag => 'a', :attributes => { :href => '/projects/1/wiki/CookBook_documentation/edit' }
833 846 end
834 847
835 848 def test_show_pdf
836 849 @request.session[:user_id] = 2
837 850 get :show, :project_id => 1, :format => 'pdf'
838 851 assert_response :success
839 852 assert_not_nil assigns(:page)
840 853 assert_equal 'application/pdf', @response.content_type
841 854 assert_equal 'attachment; filename="CookBook_documentation.pdf"',
842 855 @response.headers['Content-Disposition']
843 856 end
844 857
845 858 def test_show_html
846 859 @request.session[:user_id] = 2
847 860 get :show, :project_id => 1, :format => 'html'
848 861 assert_response :success
849 862 assert_not_nil assigns(:page)
850 863 assert_equal 'text/html', @response.content_type
851 864 assert_equal 'attachment; filename="CookBook_documentation.html"',
852 865 @response.headers['Content-Disposition']
853 866 assert_tag 'h1', :content => 'CookBook documentation'
854 867 end
855 868
856 869 def test_show_versioned_html
857 870 @request.session[:user_id] = 2
858 871 get :show, :project_id => 1, :format => 'html', :version => 2
859 872 assert_response :success
860 873 assert_not_nil assigns(:content)
861 874 assert_equal 2, assigns(:content).version
862 875 assert_equal 'text/html', @response.content_type
863 876 assert_equal 'attachment; filename="CookBook_documentation.html"',
864 877 @response.headers['Content-Disposition']
865 878 assert_tag 'h1', :content => 'CookBook documentation'
866 879 end
867 880
868 881 def test_show_txt
869 882 @request.session[:user_id] = 2
870 883 get :show, :project_id => 1, :format => 'txt'
871 884 assert_response :success
872 885 assert_not_nil assigns(:page)
873 886 assert_equal 'text/plain', @response.content_type
874 887 assert_equal 'attachment; filename="CookBook_documentation.txt"',
875 888 @response.headers['Content-Disposition']
876 889 assert_include 'h1. CookBook documentation', @response.body
877 890 end
878 891
879 892 def test_show_versioned_txt
880 893 @request.session[:user_id] = 2
881 894 get :show, :project_id => 1, :format => 'txt', :version => 2
882 895 assert_response :success
883 896 assert_not_nil assigns(:content)
884 897 assert_equal 2, assigns(:content).version
885 898 assert_equal 'text/plain', @response.content_type
886 899 assert_equal 'attachment; filename="CookBook_documentation.txt"',
887 900 @response.headers['Content-Disposition']
888 901 assert_include 'h1. CookBook documentation', @response.body
889 902 end
890 903
891 904 def test_edit_unprotected_page
892 905 # Non members can edit unprotected wiki pages
893 906 @request.session[:user_id] = 4
894 907 get :edit, :project_id => 1, :id => 'Another_page'
895 908 assert_response :success
896 909 assert_template 'edit'
897 910 end
898 911
899 912 def test_edit_protected_page_by_nonmember
900 913 # Non members can't edit protected wiki pages
901 914 @request.session[:user_id] = 4
902 915 get :edit, :project_id => 1, :id => 'CookBook_documentation'
903 916 assert_response 403
904 917 end
905 918
906 919 def test_edit_protected_page_by_member
907 920 @request.session[:user_id] = 2
908 921 get :edit, :project_id => 1, :id => 'CookBook_documentation'
909 922 assert_response :success
910 923 assert_template 'edit'
911 924 end
912 925
913 926 def test_history_of_non_existing_page_should_return_404
914 927 get :history, :project_id => 1, :id => 'Unknown_page'
915 928 assert_response 404
916 929 end
917 930
918 931 def test_add_attachment
919 932 @request.session[:user_id] = 2
920 933 assert_difference 'Attachment.count' do
921 934 post :add_attachment, :project_id => 1, :id => 'CookBook_documentation',
922 935 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
923 936 end
924 937 attachment = Attachment.first(:order => 'id DESC')
925 938 assert_equal Wiki.find(1).find_page('CookBook_documentation'), attachment.container
926 939 end
927 940 end
General Comments 0
You need to be logged in to leave comments. Login now